ssh-doctor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SSH Doctor

SSH 诊断工具

Use when SSH connects then closes before auth, Remote Login seems advertised but unusable, or local/remote Mac SSH needs diagnosis.
适用于以下场景:SSH 连接后在认证前断开、远程登录(Remote Login)看似已启用但无法使用,或者需要诊断本地/远程 Mac 的 SSH 问题。

Rules

规则

  • Do not print secrets, tokens, full env, or broad secret grep output.
  • Validate locally first: loopback failure means server-side sshd/launchd/config; loopback success plus remote failure means network/firewall/filter/listen path.
  • Report suspicious config lines before changing
    /etc/ssh/sshd_config
    .
  • Prefer non-interactive SSH:
bash
ssh -o RequestTTY=no -o RemoteCommand=none HOST 'hostname; id -un'
  • 请勿输出密钥、令牌、完整环境变量或大范围的密钥 grep 搜索结果。
  • 优先本地验证:回环连接失败说明是服务端 sshd/launchd/配置问题;回环连接成功但远程连接失败说明是网络/防火墙/过滤规则/监听路径问题。
  • 在修改
    /etc/ssh/sshd_config
    之前,先上报可疑的配置行。
  • 优先使用非交互式 SSH:
bash
ssh -o RequestTTY=no -o RemoteCommand=none HOST 'hostname; id -un'

Baseline

基线检查

bash
hostname; id -un; sw_vers
ipconfig getifaddr en0
ipconfig getifaddr en1 2>/dev/null || true
ipconfig getifaddr en7 2>/dev/null || true
sudo systemsetup -getremotelogin
sudo systemsetup -setremotelogin on
sudo launchctl print system/com.openssh.sshd 2>&1 | head -80
sudo launchctl kickstart -k system/com.openssh.sshd
sudo lsof -nP -iTCP:22 -sTCP:LISTEN
nc -vz 127.0.0.1 22
ssh -4 -F /dev/null -o RequestTTY=no -o RemoteCommand=none USER@127.0.0.1 'hostname; id -un'
Use
BatchMode=yes
only when password fallback would hang or prompt.
bash
hostname; id -un; sw_vers
ipconfig getifaddr en0
ipconfig getifaddr en1 2>/dev/null || true
ipconfig getifaddr en7 2>/dev/null || true
sudo systemsetup -getremotelogin
sudo systemsetup -setremotelogin on
sudo launchctl print system/com.openssh.sshd 2>&1 | head -80
sudo launchctl kickstart -k system/com.openssh.sshd
sudo lsof -nP -iTCP:22 -sTCP:LISTEN
nc -vz 127.0.0.1 22
ssh -4 -F /dev/null -o RequestTTY=no -o RemoteCommand=none USER@127.0.0.1 'hostname; id -un'
仅当密码回退会导致挂起或弹出提示时,才使用
BatchMode=yes
。

Config

配置检查

bash
sudo sshd -T 2>&1 | egrep -i '^(allowusers|denyusers|allowgroups|denygroups|listenaddress|maxstartups|logingracetime|usepam|passwordauthentication|pubkeyauthentication|authenticationmethods)'
sudo egrep -n '^[[:space:]]*(AllowUsers|DenyUsers|AllowGroups|DenyGroups|Match|MaxStartups|LoginGraceTime|ListenAddress|AuthenticationMethods|UsePAM|PasswordAuthentication|PubkeyAuthentication)\b' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/* 2>/dev/null || true
Suspicious:
  • DenyUsers
    matching target user
  • restrictive
    AllowUsers
    /
    AllowGroups
  • Match
    block accidentally applying
  • tiny
    MaxStartups
  • tiny
    LoginGraceTime
  • ListenAddress
    missing target interface
bash
sudo sshd -T 2>&1 | egrep -i '^(allowusers|denyusers|allowgroups|denygroups|listenaddress|maxstartups|logingracetime|usepam|passwordauthentication|pubkeyauthentication|authenticationmethods)'
sudo egrep -n '^[[:space:]]*(AllowUsers|DenyUsers|AllowGroups|DenyGroups|Match|MaxStartups|LoginGraceTime|ListenAddress|AuthenticationMethods|UsePAM|PasswordAuthentication|PubkeyAuthentication)\b' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/* 2>/dev/null || true
可疑情况:
  • DenyUsers
    匹配到目标用户
  • 限制严格的
    AllowUsers
    /
    AllowGroups
    配置
  • Match
    块意外生效
  • MaxStartups
    值过小
  • LoginGraceTime
    值过小
  • ListenAddress
    缺少目标网络接口

Logs

日志检查

bash
sudo log show --last 30m --predicate 'process == "sshd" OR process == "launchd"' --style compact | tail -160
Important Mac symptom:
  • client:
    kex_exchange_identification: Connection closed by remote host
  • server log:
    Could not create new instance of inetd service: 67: Too many processes
  • launchctl print system/com.openssh.sshd
    : high
    copy count
  • many
    sshd-session: USER
    processes parented by PID 1
This means launchd accepted TCP but refused to spawn more sshd inetd copies.
bash
sudo log show --last 30m --predicate 'process == "sshd" OR process == "launchd"' --style compact | tail -160
Mac 上的重要症状:
  • 客户端报错:
    kex_exchange_identification: Connection closed by remote host
  • 服务端日志:
    Could not create new instance of inetd service: 67: Too many processes
  • launchctl print system/com.openssh.sshd
    输出中
    copy count
    过高
  • 大量父进程为 PID 1 的
    sshd-session: USER
    进程
这意味着 launchd 接受了 TCP 连接,但拒绝生成更多的 sshd inetd 实例。

Stale sshd-session Fix

僵死 sshd-session 修复

Inspect first:
bash
sudo launchctl print system/com.openssh.sshd 2>&1 | egrep 'active count|copy count|state =|last exit code|runs ='
ps -axo pid,ppid,uid,user,state,lstart,etime,comm,args | awk '/sshd-session:/ && !/awk/ {print}'
sudo lsof -nP -c sshd-session -iTCP 2>/dev/null | head -120
If stale sessions are clearly stranded and blocking new SSH, terminate by selected command-line match:
bash
ps -axo pid=,args= | awk '/sshd-session: / && !/awk/ {print $1}' | xargs sudo kill -TERM
sleep 2
ps -axo pid=,args= | awk '/sshd-session: / && !/awk/ {print}'
If
TERM
leaves blockers, re-check ownership and active shells before using
KILL
.
先执行检查:
bash
sudo launchctl print system/com.openssh.sshd 2>&1 | egrep 'active count|copy count|state =|last exit code|runs ='
ps -axo pid,ppid,uid,user,state,lstart,etime,comm,args | awk '/sshd-session:/ && !/awk/ {print}'
sudo lsof -nP -c sshd-session -iTCP 2>/dev/null | head -120
如果确认存在僵死会话且阻塞了新的 SSH 连接,可通过匹配命令行来终止对应进程:
bash
ps -axo pid=,args= | awk '/sshd-session: / && !/awk/ {print $1}' | xargs sudo kill -TERM
sleep 2
ps -axo pid=,args= | awk '/sshd-session: / && !/awk/ {print}'
如果
TERM
信号未能清除阻塞进程,在使用
KILL
信号前请重新检查进程归属和活跃的 shell。

Firewall

防火墙检查

Only after loopback works but remote fails:
bash
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --listapps | grep -i ssh -A2 -B2 || true
sudo pfctl -sr 2>/dev/null | head -80
sudo pfctl -si 2>/dev/null | head -80
Also check listen address and target interface:
bash
ifconfig | awk '/^[a-z0-9]+:/{iface=$1; sub(":","",iface)} iface ~ /^en[0-9]+$/ && /inet / {print iface, $2}'
sudo lsof -nP -iTCP:22 -sTCP:LISTEN
仅在回环连接正常但远程连接失败时执行:
bash
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --listapps | grep -i ssh -A2 -B2 || true
sudo pfctl -sr 2>/dev/null | head -80
sudo pfctl -si 2>/dev/null | head -80
同时检查监听地址和目标网络接口:
bash
ifconfig | awk '/^[a-z0-9]+:/{iface=$1; sub(":","",iface)} iface ~ /^en[0-9]+$/ && /inet / {print iface, $2}'
sudo lsof -nP -iTCP:22 -sTCP:LISTEN

OP Profile Block

OP 配置文件块

If asked to ensure
~/.profile
has a Codex-managed
OP_SERVICE_ACCOUNT_TOKEN
copied from another host:
  • verify exact variable/markers without printing value
  • copy only the matching line/block
  • redirect through a
    chmod 600
    temp file
  • never echo the token
Presence check:
bash
awk 'BEGIN{b=0;e=0;x=0} /BEGIN Codex-managed OP_SERVICE_ACCOUNT_TOKEN/ {b=1} /END Codex-managed OP_SERVICE_ACCOUNT_TOKEN/ {e=1} /^[[:space:]]*(export[[:space:]]+)?OP_SERVICE_ACCOUNT_TOKEN=/ {x=1} END{print "marker_begin", b; print "marker_end", e; print "exact_var", x}' ~/.profile
Append from remote host:
bash
tmpfile=$(mktemp /tmp/codex-op-token.XXXXXX)
chmod 600 "$tmpfile"
ssh -o RequestTTY=no -o RemoteCommand=none HOST 'awk '\''/^[[:space:]]*(export[[:space:]]+)?OP_SERVICE_ACCOUNT_TOKEN=/ {print; exit}'\'' ~/.profile' > "$tmpfile"
if [ -s "$tmpfile" ]; then
  {
    printf '\n# BEGIN Codex-managed OP_SERVICE_ACCOUNT_TOKEN\n'
    sed -n '1p' "$tmpfile"
    printf '# END Codex-managed OP_SERVICE_ACCOUNT_TOKEN\n'
  } >> ~/.profile
fi
rm -f "$tmpfile"
如果需要确保
~/.profile
中包含从另一台主机复制的、由 Codex 管理的
OP_SERVICE_ACCOUNT_TOKEN
:
  • 验证准确的变量/标记,但不输出其值
  • 仅复制匹配的行/块
  • 通过权限为
    chmod 600
    的临时文件重定向
  • 绝对不要 echo 令牌内容
存在性检查:
bash
awk 'BEGIN{b=0;e=0;x=0} /BEGIN Codex-managed OP_SERVICE_ACCOUNT_TOKEN/ {b=1} /END Codex-managed OP_SERVICE_ACCOUNT_TOKEN/ {e=1} /^[[:space:]]*(export[[:space:]]+)?OP_SERVICE_ACCOUNT_TOKEN=/ {x=1} END{print "marker_begin", b; print "marker_end", e; print "exact_var", x}' ~/.profile
从远程主机追加:
bash
tmpfile=$(mktemp /tmp/codex-op-token.XXXXXX)
chmod 600 "$tmpfile"
ssh -o RequestTTY=no -o RemoteCommand=none HOST 'awk '\''/^[[:space:]]*(export[[:space:]]+)?OP_SERVICE_ACCOUNT_TOKEN=/ {print; exit}'\'' ~/.profile' > "$tmpfile"
if [ -s "$tmpfile" ]; then
  {
    printf '\n# BEGIN Codex-managed OP_SERVICE_ACCOUNT_TOKEN\n'
    sed -n '1p' "$tmpfile"
    printf '# END Codex-managed OP_SERVICE_ACCOUNT_TOKEN\n'
  } >> ~/.profile
fi
rm -f "$tmpfile"

Closeout

收尾报告

Report:
  • root cause
  • exact commands changed
  • validation output, redacted as needed
  • whether remote should retry
需报告的内容:
  • 根本原因
  • 执行过的所有修改命令
  • 验证输出(按需脱敏)
  • 是否可以重试远程连接