systemd ssh.socket override s ListenStream odřízne SSH bez možnosti vzdálené recovery
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”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 :22na lokální konzoli (kdyby tam byla) vrací prázdnosystemctl status ssh.socket→failedse zprávou typuJob for ssh.socket failed- Žádná vzdálená cesta zpět — admin je odříznutý
Root cause
Sekce “Root cause”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=22Accept=noKdyž admin chce přidat alt port (např. 2222 pro bypass GeoIP whitelistu), často sáhne po systemd drop-in override:
mkdir /etc/systemd/system/ssh.socket.dcat > /etc/systemd/system/ssh.socket.d/listen.conf <<EOF[Socket]ListenStream=2222EOFsystemctl daemon-reloadsystemctl restart ssh.socketDrop-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=noneumí 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.
Konkrétní incident
Sekce “Konkrétní incident”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.
Fix
Sekce “Fix”Akutní recovery
Sekce “Akutní recovery”- Aktivovat rescue boot u poskytovatele (Hetzner Robot → Rescue → Linux 64bit, zkopírovat jednorázové heslo)
- Reboot do rescue (Hetzner Robot → Reset → CTRL+ALT+DEL nebo hardware reset)
- SSH do rescue Linuxu s rescue heslem
- Mount root filesystem:
Terminál lsblk # identifikuj root devicemkdir /mntmount /dev/mapper/vg0-root /mnt # uprav podle lsblk - Smazat problematický override:
Terminál rm /mnt/etc/systemd/system/ssh.socket.d/listen.confrmdir /mnt/etc/systemd/system/ssh.socket.dumount /mnt - Reboot zpět do normálního OS (rescue je jednorázový)
ssh.socketse 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:
cat > /etc/systemd/system/ssh-extra.socket <<EOF[Unit]Description=Extra SSH listener (alt port 2222 — bypass GeoIP)
[Socket]ListenStream=2222Accept=no
[Install]WantedBy=sockets.targetEOF
systemctl daemon-reloadsystemctl 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:
# /etc/ufw/before.rules (pod *filter sekci):-A ufw-before-input -p tcp --dport 2222 -j ACCEPTufw reloadAvoidance
Sekce “Avoidance”Před každou změnou sshd/systemd na produkci
Sekce “Před každou změnou sshd/systemd na produkci”- Otevři rescue/console UI poskytovatele PŘEDEM — Hetzner Robot, Linode LISH, OVH IPMI, atd. Aspoň ověř že máš credentials.
- Otevři DRUHÉ SSH session do serveru. Přežije
systemctl restart(i kdyby nový listener selhal). Nepřežije reboot. sshd -tnebosystemd-analyze verify <unit>PŘED každým restartem.- Test z TŘETÍHO terminálu/zařízení (jiná IP, jiný klient) že nový SSH funguje, PAK teprve odhlas druhý session.
- Pro socket-activated SSH NIKDY nepřidávej
ListenStream=přes drop-in override — vždy vlastní.socketunit. systemctl status ssh.socket && ss -tlnp | grep sshpo 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.
Anti-pattern
Sekce “Anti-pattern”- Mixovat klasickou
sshd.service(long-running) a socket-activatedssh.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 reloadu service co podporuje jenrestart— graceful není garantovaný.
Sister bugs
Sekce “Sister bugs”- 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
ignoreipv/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”.
Klíčové ponaučení
Sekce “Klíčové ponaučení”Socket-activated služby (
ssh.socket,nginx.socket, atd.) mají specifickou systemd sémantiku. Drop-in overrides sListenStream=nepřidávají listenery, ale přepisují je — a často selhávají. Vždy vytvoř NOVÝ.socketunit pro additional listenery, ne override existujícího.