Přeskočit na obsah

systemd ssh.socket override s ListenStream odřízne SSH bez možnosti vzdálené recovery

import { Aside } from ‘@astrojs/starlight/components’;

Po systemctl restart ssh.socket na moderním Debian/Ubuntu (22.04+):

  • SSH na port 22 odpovídá Connection refused (NE timeout — to by bylo network/firewall blok)
  • ICMP ping <ip> ✅ — server běží, OS pořád funguje
  • HTTPS port 443 ✅ — nginx/Caddy/Apache dál obsluhuje uživatele
  • PM2/aplikační procesy v memory pořád běží — uživatelé nic nepoznají
  • ss -tlnp | grep :22 na lokální konzoli (kdyby tam byla) vrací prázdno
  • systemctl status ssh.socketfailed se zprávou typu Job for ssh.socket failed
  • Žádná vzdálená cesta zpět — admin je odříznutý

Moderní Debian/Ubuntu distribuce přešly z klasické sshd.service (proces co listenuje na portu permanentně) na socket-activated SSH přes ssh.socket systemd unit:

ssh.socket → triggers ssh@.service per connection (on-demand)

Defaultní ssh.socket má sémantiku:

[Socket]
ListenStream=22
Accept=no

Když admin chce přidat alt port (např. 2222 pro bypass GeoIP whitelistu), často sáhne po systemd drop-in override:

Terminál
mkdir /etc/systemd/system/ssh.socket.d
cat > /etc/systemd/system/ssh.socket.d/listen.conf <<EOF
[Socket]
ListenStream=2222
EOF
systemctl daemon-reload
systemctl restart ssh.socket

Drop-in ListenStream= má pro Socket sekci PŘEPSÁVACÍ chování — ne aditivní. Druhá hodnota nahrazuje první, ne přidává vedle. Plus Accept=no semantika počítá s jediným socket per ssh@.service triggerem.

Restart může selhat z několika důvodů:

  • Stávající instance sshd drží port 22 (PID konflikt při reload)
  • Konflikt s existujícím listenerem v jiném procesu
  • Accept=no neumí dva ListenStream současně bez templating

Výsledek: ssh.socket zůstane failed. Žádný listener — žádné ssh@.service triggery — žádný SSH přístup.

Server zdravě běží, jen nepřístupný vzdáleně. Recovery vyžaduje fyzický/rescue přístup.

Production server (EU dedicated, Hetzner). Admin byl na Starlink internetu (US/EU exit IP), server měl iptables GeoIP whitelist port 22 jen z CZ. SSH timeout z non-CZ IP.

Admin se přepnul na mobilní CZ síť, otevřel SSH, chtěl vytvořit alt port 2222 bez GeoIP. Vytvořil override file s ListenStream=2222, spustil systemctl restart ssh.socket → fail.

Server byl odříznutý jak ze Starlinku (GeoIP), tak z mobilního CZ (žádný listener). Total downtime CRM ~10 min během recovery přes rescue.

  1. Aktivovat rescue boot u poskytovatele (Hetzner Robot → Rescue → Linux 64bit, zkopírovat jednorázové heslo)
  2. Reboot do rescue (Hetzner Robot → Reset → CTRL+ALT+DEL nebo hardware reset)
  3. SSH do rescue Linuxu s rescue heslem
  4. Mount root filesystem:
    Terminál
    lsblk # identifikuj root device
    mkdir /mnt
    mount /dev/mapper/vg0-root /mnt # uprav podle lsblk
  5. Smazat problematický override:
    Terminál
    rm /mnt/etc/systemd/system/ssh.socket.d/listen.conf
    rmdir /mnt/etc/systemd/system/ssh.socket.d
    umount /mnt
  6. Reboot zpět do normálního OS (rescue je jednorázový)
  7. ssh.socket se v normálním boot načte bez override → port 22 listenuje → SSH funguje

Správný způsob přidat alt SSH port

Sekce “Správný způsob přidat alt SSH port”

Vytvoř NOVÝ samostatný .socket unit, ne override existujícího:

Terminál
cat > /etc/systemd/system/ssh-extra.socket <<EOF
[Unit]
Description=Extra SSH listener (alt port 2222 — bypass GeoIP)
[Socket]
ListenStream=2222
Accept=no
[Install]
WantedBy=sockets.target
EOF
systemctl daemon-reload
systemctl enable --now ssh-extra.socket
# Ověř
ss -tlnp | grep -E ":22 |:2222 "

Tento přístup vytvoří vlastní socket vedle ssh.socket. Oba mohou nezávisle triggerovat ssh@.service. Žádný konflikt, žádný drop-in problém. Pokud ssh-extra.socket selže, existující ssh.socket na port 22 je nedotčený.

Plus do UFW povolit nový port:

Terminál
# /etc/ufw/before.rules (pod *filter sekci):
-A ufw-before-input -p tcp --dport 2222 -j ACCEPT
ufw reload

Před každou změnou sshd/systemd na produkci

Sekce “Před každou změnou sshd/systemd na produkci”
  1. Otevři rescue/console UI poskytovatele PŘEDEM — Hetzner Robot, Linode LISH, OVH IPMI, atd. Aspoň ověř že máš credentials.
  2. Otevři DRUHÉ SSH session do serveru. Přežije systemctl restart (i kdyby nový listener selhal). Nepřežije reboot.
  3. sshd -t nebo systemd-analyze verify <unit> PŘED každým restartem.
  4. Test z TŘETÍHO terminálu/zařízení (jiná IP, jiný klient) že nový SSH funguje, PAK teprve odhlas druhý session.
  5. Pro socket-activated SSH NIKDY nepřidávej ListenStream= přes drop-in override — vždy vlastní .socket unit.
  6. systemctl status ssh.socket && ss -tlnp | grep ssh po každé změně — kontrola že listenery jsou kde mají být.

Detection signal pro monitoring

Sekce “Detection signal pro monitoring”

Alert pokud:

ss -tlnp | grep ":22 " | wc -l == 0 (žádný SSH listener)
+
ping <server> ✅ (server up)
+
curl <server> ✅ (web up)

Kombinace “server up + no SSH listener” = pravděpodobně tenhle bug. Notifikace adminovi + auto-aktivace rescue přes provider API (Hetzner Robot, atd.) může ušetřit minuty.

  • Mixovat klasickou sshd.service (long-running) a socket-activated ssh.socket (on-demand) na stejném portu. Distribuce volí jednu z těchto cest při instalaci — drž se jí.
  • Měnit SSH config “na živo” bez backup SSH key path nebo otevřeného secondary session.
  • Spoléhat na systemctl reload u service co podporuje jen restart — graceful není garantovaný.
  • GeoIP iptables admin lockout: typický pattern u EU dedicated serverů s anti-bot ochranou. Admin v zahraničí nemůže přístup. Řešení: ipset whitelist + alt port + helper script pro ad-hoc IP add.
  • fail2ban self-lockout: vysoký počet rychlých SSH connections (např. AI agent dispatch) může spustit fail2ban ban na admin IP. Whitelist ignoreip v /etc/fail2ban/jail.local.
  • systemd socket-activated mail server / databáze mají stejnou past s drop-in ListenStream= overrides. Universal princip “nový unit, ne override”.

Socket-activated služby (ssh.socket, nginx.socket, atd.) mají specifickou systemd sémantiku. Drop-in overrides s ListenStream= nepřidávají listenery, ale přepisují je — a často selhávají. Vždy vytvoř NOVÝ .socket unit pro additional listenery, ne override existujícího.

Přidal aiarchitekt.cz · 9. 6. 2026 2:00
Provozuje aiarchitekt.cz