デプロイのたびにSSHパスフレーズを聞かれる――真犯人はagentの生存判定だった
デプロイスクリプトでSSH鍵のパスフレーズを毎回聞かれる場合、最初に疑いたくなるのは「鍵がssh-agentに載っているか」の判定です。
ただ、今回の原因候補はその一段手前でした。既存のssh-agentが生きているかを判定する処理が失敗し、毎回新しいagentを起動していたため、結果として毎回鍵を読み込む流れになっていた可能性が高い、というケースです。
この記事では、複数サイトをまとめてデプロイするbashスクリプトを前提に、ssh-agent再利用の切り分け方と、kill -0 を使った生存確認への置き換えを整理します。
- 対象: Git + SSH鍵 + ssh-agent を使うデプロイスクリプト
- 症状: 同一ログインセッション内でも、デプロイのたびにSSH鍵のパスフレーズを聞かれる
- 修正: agentの生存判定を
ps系コマンドからkill -0に変更 - 確認:
bash -nは通過し、本番環境での動作確認も成功
「対策したのに毎回パスフレーズ」その時どこを疑うか
今回のポイントは、鍵そのものではなく、鍵を保持している ssh-agentを再利用できているか です。
スクリプトには、すでに次のような改修が入っていました。
- 鍵がすでにagentに読み込まれているかを判定する
- 未ロードなら
ssh-addで鍵を読み込む - 鍵には有効期限を付ける
一見すると、これで「一度パスフレーズを入力したら、しばらく再入力しない」動きになりそうです。
しかし、その処理に到達する前に、スクリプトが毎回「既存agentは使えない」と判断していたらどうなるでしょうか。
流れはこうなります。
- 保存済みのagent情報を読み込む
- agentの生存判定に失敗する
- 新しいssh-agentを起動する
- 新しいagentには鍵が載っていない
ssh-addが必要になる- パスフレーズを聞かれる
つまり、鍵ロード判定を改善しても、その手前のagent再利用判定が壊れていると効果が出ません。
ここがポイント: 「鍵が載っているか」だけでなく、「同じagentを見に行けているか」を確認する必要があります。
ssh-agentで鍵入力を1回にする仕組みのおさらい
ssh-agentは、SSH秘密鍵の認証情報を一時的に保持する常駐プロセスです。
パスフレーズ付きの秘密鍵を使う場合でも、一度 ssh-add で鍵を読み込めば、そのagentプロセスが生きている間は再入力を避けられます。
デプロイスクリプトでは、だいたい次のような構造になります。
# 保存済みのagent環境情報を読み込む
source "$HOME/.ssh/.agent_env"
# 既存agentが使えなければ新規起動する
eval "$(ssh-agent -s)"
# 必要なときだけ鍵を追加する
ssh-add -t 8h "$HOME/.ssh/id_ed25519_example"
実際の運用では、SSH_AUTH_SOCK と SSH_AGENT_PID をファイルに保存しておき、次回実行時に読み込みます。
期待する動きは次の通りです。
- 同一ログインセッション内では、既存agentを再利用する
- agentに鍵が載っていれば、
ssh-addしない - 鍵が未ロードなら、その時だけパスフレーズを入力する
- ログインし直した後に再入力が必要になるのは許容する
今回問題にしたのは、最後の「ログインし直した後」ではありません。同じセッション内で続けて実行しても毎回聞かれる、という点です。
改修が“実行に到達していなかった”という盲点
直前の改修では、鍵ロード周りの処理が追加されていました。
具体的には、既存agentに鍵が載っているかを確認し、必要な場合だけ有効期限付きで鍵を読み込む処理です。
ただし、agentの生存判定そのものは変更されていませんでした。
ここが盲点でした。
デプロイ処理の分岐を分解する
ssh-agentを使うデプロイスクリプトでは、処理がいくつかの段階に分かれます。
- 保存した環境情報ファイルを読み込む
SSH_AGENT_PIDのプロセスが生きているか確認するSSH_AUTH_SOCKが使えるか確認する- Gitリモートへアクセスできるか確認する
- 必要なら
ssh-addする git fetchやgit resetを実行する
今回のように「毎回パスフレーズを聞かれる」場合、ssh-add の条件式だけを見ると遠回りになります。
先に見るべきなのは、保存済みのagent情報が実際に再利用されているかです。
毎回新しいagentなら、鍵は毎回空になる
新しく起動したssh-agentには、当然まだ鍵が載っていません。
そのため、鍵ロード判定は正しく「未ロード」と判断します。ここだけ見ると処理は正常です。
しかし原因はその前にあります。既存agentを使えるはずなのに、使えないと判定してしまう。その結果、毎回空のagentを作り直していた可能性が高い、という見立てです。
プロセス生存判定の落とし穴
今回のスクリプトでは、agentの生存判定に ps 系のコマンドを特定の出力指定付きで使っていました。
ps 自体が悪いわけではありません。ただ、オプションや出力指定は環境によって差が出ることがあります。
たとえば、Linuxディストリビューション、BusyBox系の環境、コンテナ、ホスティング環境などでは、使える ps のオプションや出力形式が異なる場合があります。
今回の症状は、次の連鎖だった可能性があります。
- 保存済みの
SSH_AGENT_PIDは存在する - しかし
psによる確認が環境依存で失敗する - スクリプトは「agentが生きていない」と判断する
- 新しいagentを起動する
- 鍵が空なので
ssh-addが走る - パスフレーズ入力が毎回発生する
これは推測を含む説明ですが、修正後に本番環境で成功しているため、今回の切り分けとしては妥当な原因候補でした。
単純で堅牢な存在確認に置き換える
修正では、agentの生存判定を kill -0 に置き換えました。
kill -0 <PID> は、プロセスに実際の終了シグナルを送るのではなく、そのPIDに対してシグナル送信できるかを確認する用途で使えます。
デプロイスクリプト内では、次のように確認します。
if [[ -n "${SSH_AGENT_PID:-}" ]] \
&& kill -0 "${SSH_AGENT_PID}" 2>/dev/null \
&& [[ -S "${SSH_AUTH_SOCK:-/dev/null}" ]]; then
echo ">>> Using existing ssh-agent"
return
fi
ここで見ているのは2点です。
SSH_AGENT_PIDのプロセスが存在するかSSH_AUTH_SOCKがUnixドメインソケットとして存在するか
PID だけでは、別プロセスに置き換わっている可能性を完全には排除できません。そのため、socketの存在も合わせて見ています。
今回の目的は「セッションを跨いでagentを永続化する」ことではなく、同一セッション内で不要な再入力を避けることです。linger などのセッション管理を変える話には踏み込んでいません。
サンプル: ensure_agentの考え方
固有のパスやサイト名を一般化すると、修正後の考え方は次のようになります。
ensure_agent() {
if [[ -f "${AGENT_ENV}" ]]; then
# shellcheck disable=SC1090
source "${AGENT_ENV}" >/dev/null 2>&1 || true
fi
if [[ -n "${SSH_AGENT_PID:-}" ]] \
&& kill -0 "${SSH_AGENT_PID}" 2>/dev/null \
&& [[ -S "${SSH_AUTH_SOCK:-/dev/null}" ]]; then
echo ">>> Using existing ssh-agent (pid: ${SSH_AGENT_PID})"
return
fi
echo ">>> Starting new ssh-agent"
eval "$(ssh-agent -s)" >/dev/null
umask 077
{
echo "export SSH_AUTH_SOCK=${SSH_AUTH_SOCK}"
echo "export SSH_AGENT_PID=${SSH_AGENT_PID}"
} > "${AGENT_ENV}"
}
AGENT_ENV にはagentの接続情報を書き出します。認証情報そのものを書くわけではありませんが、ssh-agentに接続するための情報なので、umask 077 で他ユーザーから読みにくくしておくのが無難です。
同一セッション連続実行で検証する
この種の修正は、1回だけ実行しても十分に確認できません。
確認したいのは、「1回目に必要ならパスフレーズを聞かれ、2回目以降は同一セッション内で聞かれない」ことです。
最低限の確認手順
対象はbashスクリプトです。実行場所は、デプロイスクリプトを置いているサーバー上を想定します。
bash -n multi-deploy.sh
まず構文チェックを通します。今回の修正では、この bash -n は通過しています。
次に、同じログインセッション内で連続実行します。
./multi-deploy.sh site-a main
./multi-deploy.sh site-a main
見るポイントは次の通りです。
- 1回目に必要ならパスフレーズを聞かれる
- 2回目で既存agentを使っているログが出る
- 2回目でパスフレーズを聞かれない
- Gitリモートへのアクセス確認が通る
- fetch / checkout / reset が最後まで完了する
今回のケースでは、ユーザーが本番環境で動作確認を行い、成功したと報告しています。
注意: git reset –hard を含むスクリプトは実行前に対象を確認する
デプロイスクリプトには、作業ツリーをクリーンにする目的で git reset --hard HEAD や git reset --hard origin/<branch> が含まれることがあります。
これは未コミットの変更を消す操作です。実行前に、少なくとも次を確認しておきます。
git status --short
git remote -v
git branch --show-current
デプロイ専用ディレクトリであれば問題になりにくいですが、手作業の変更が混ざる運用では危険です。
代替案とトレードオフ
今回採用したのは、既存の構造を大きく変えずに、生存判定だけを置き換える方法です。
他にも選択肢はあります。
ssh-add -lを中心に判定する- agent環境ファイルを毎回破棄して起動し直す
- セッションを跨いでagentを残す仕組みを検討する
- OSやログイン管理側でagentを扱う
ただし、今回の要件は「同一セッション内で毎回聞かれないようにする」ことでした。
そのため、セッションを跨ぐ永続化までは採用していません。運用上の便利さは増えるかもしれませんが、鍵の保持時間、ログアウト後の扱い、サーバー上のユーザー分離など、別の確認事項が増えるためです。
kill -0 も万能ではありません。PID再利用や権限の問題を完全に吸収するものではないため、SSH_AUTH_SOCK の確認や、実際の git ls-remote による接続確認と組み合わせて見るのが現実的です。
再発防止で見るポイント
同じ問題を再度調べるときは、いきなり ssh-add から見ないほうが早いです。
次の順で確認すると、分岐の破綻箇所を見つけやすくなります。
- 保存済みのagent環境ファイルが読み込まれているか
SSH_AGENT_PIDが毎回変わっていないかSSH_AUTH_SOCKが存在しているか- 既存agentを使う分岐に入っているか
ssh-add -lで鍵が見えているかgit ls-remote -h origin HEADが通るか- 2回連続実行したとき、2回目でパスフレーズを聞かれないか
ログには、最低限次の情報を出しておくと切り分けが楽になります。
echo ">>> Using existing ssh-agent (pid: ${SSH_AGENT_PID}, sock: ${SSH_AUTH_SOCK})"
echo ">>> Existing ssh-agent not usable -> starting new ssh-agent"
echo ">>> SSH key already loaded in agent"
echo ">>> Adding SSH key (passphrase may be requested once)"
秘密鍵のパスやリモートURLをログに出す場合は、公開してよい情報か確認してください。パスフレーズや秘密鍵の中身を出力してはいけません。
まとめ
今回の教訓はシンプルです。
「鍵を読み込む処理」を直しても効かないときは、その手前の「同じagentを再利用できているか」を見る必要があります。
特に、次のような状況ではagent生存判定を疑う価値があります。
- 同一セッション内なのに毎回パスフレーズを聞かれる
ssh-add周りを直しても改善しない- 実行のたびに
SSH_AGENT_PIDが変わっている - agent環境ファイルはあるのに再利用されていない
ps系の判定を環境依存のオプション付きで使っている
今回の環境では、agentの生存確認を kill -0 に置き換え、bash -n を通したうえで、本番環境での連続実行に成功しました。
次に同じ症状を見たら、まず「鍵が載っているか」ではなく、「そもそも前回と同じagentを見ているか」から確認します。
参照リンク
- 本文中で参照した外部URLはありません。

コメント