ssh-doctor について
SSH Doctor は、認証前に切断される SSH 接続などを診断するスキルで、ローカルおよびリモートの Mac を対象とします。sshd、launchd、設定、ログ、ネットワークフィルターの確認を案内し、秘密情報や設定変更の扱いにも配慮します。
主な機能
- 01SSH サービスと設定の確認
- 02ローカル障害とネットワーク障害の切り分け
- 03sshd と launchd のログ確認
- 04古い SSH セッションの特定
- 05診断時の秘密情報保護
活用シーン
- 01認証前に切断される SSH の診断
- 02Mac のリモートログイン障害の調査
- 03新規接続を妨げる古いセッションの特定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ steipete/ agent-scripts --skill ssh-doctorインストール後、Claude Code で/ssh-doctorを入力して読み込まれたか確認します。
詳細
SSH Doctor
SSH が認証前に接続後すぐ切断される場合、Remote Login が有効に見えるのに使用できない場合、またはローカル/リモートの Mac の SSH を診断する場合に使用します。
ルール
- シークレット、トークン、環境変数全体、または広範囲なシークレット検索の出力を表示しないでください。
- まずローカルで検証します。ループバック接続に失敗する場合はサーバー側の sshd/launchd/設定が原因です。ループバック接続が成功し、リモート接続に失敗する場合はネットワーク/ファイアウォール/フィルター/リッスン経路が原因です。
/etc/ssh/sshd_configを変更する前に、疑わしい設定行を報告してください。- 非対話型 SSH を優先します。
ssh -o RequestTTY=no -o RemoteCommand=none HOST 'hostname; id -un'
基本確認
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 [email protected] 'hostname; id -un'
パスワードへのフォールバック時に処理が停止したりプロンプトが表示されたりする場合に限り、BatchMode=yes を使用してください。
設定
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に対象インターフェイスが指定されていない
ログ
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 のコピーを起動できずに拒否したことを意味します。
残留した sshd-session の修正
まず確認します。
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 接続を妨げている場合は、選択したコマンドライン一致条件で終了します。
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 を使用する前に所有者とアクティブなシェルを再確認してください。
ファイアウォール
ループバック接続は成功するがリモート接続に失敗する場合に限り、確認します。
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
リッスンアドレスと対象インターフェイスも確認します。
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 プロファイルブロック
別のホストからコピーした Codex 管理の OP_SERVICE_ACCOUNT_TOKEN が ~/.profile にあることを確認するよう依頼された場合:
- 値を表示せずに、正確な変数/マーカーを確認する
- 一致する行/ブロックのみをコピーする
chmod 600を適用した一時ファイルを経由してリダイレクトする- トークンを絶対にエコーしない
存在確認:
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
リモートホストから追記:
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"
完了時の報告
次の内容を報告します。
- 根本原因
- 変更した正確なコマンド
- 検証出力(必要に応じて編集)
- リモート側で再試行すべきかどうか
コメント
Markdown 対応