Claude Media
Claude Codeでssh configを整える — ProxyJumpと鍵を渡さない運用

Claude Codeでssh configを整える — ProxyJumpと鍵を渡さない運用

~/.ssh/configをホスト別に書き、ProxyJumpで踏み台を通し、秘密鍵はReadの拒否ルールとsandboxのcredentialsで隠す。Claudeにはホスト名だけを渡す設定例をまとめます。

Claude Codeにサーバー作業を任せるとき、鍵の中身を見せる必要はありません。~/.ssh/configにホスト別の設定を書いて、Claudeにはssh app-stgというホスト名だけを渡す。鍵ファイルはReadの拒否ルールで隠す。この分担にすると、踏み台(ProxyJump)を挟んだ接続も、鍵を渡さずに動かせます。

本記事は、configの書き方、踏み台の設定で踏みやすい点、鍵を読ませない拒否設定の順にまとめます。

Claudeに渡すのはホスト名だけにする

SSHの接続先、ユーザー、鍵、踏み台は、すべて~/.ssh/configのHostブロックに寄せられます。ここに寄せておけば、Claudeが打つコマンドはssh app-stg uptimeのように短くなります。コマンドラインに-i ~/.ssh/id_ed25519_appのような鍵のパスを書かせる必要がなくなり、許可ルールも書きやすくなります。

役割は次のように分かれます。

置き場所持たせるものClaudeに見せるか
~/.ssh/config持たせるものホスト名、ユーザー、踏み台、鍵のパスClaudeに見せるか見せてよい(秘密ではない)
~/.ssh/keys/持たせるもの秘密鍵そのものClaudeに見せるか見せない(Read拒否)
~/.claude/settings.json(ユーザー設定)持たせるもの拒否・確認のルールClaudeに見せるか常に読まれる側

configには鍵の「パス」しか書かれません。パスは秘密ではないので、Claudeが読めても困りません。隠すのは鍵ファイルの中身だけです。

ホスト別のconfigと、ProxyJumpの書き方

踏み台(bastion)を1台挟み、その奥にステージング用のappサーバーがある構成を例にします。ホスト名とIPは例示用の値です。

# ~/.ssh/config
Host bastion
  HostName bastion.example.com
  User deploy
  IdentityFile ~/.ssh/keys/id_ed25519_bastion
  IdentitiesOnly yes
 
Host app-stg
  HostName 10.0.1.20
  User deploy
  ProxyJump bastion
  IdentityFile ~/.ssh/keys/id_ed25519_app
  IdentitiesOnly yes
 
Host *
  ServerAliveInterval 30

ポイントは3つです。

  • ProxyJump bastionは、bastionへの接続を作り、そこから最終の宛先へTCP転送する指定です。user@host:portの形でも書け、カンマ区切りで複数の踏み台を順に通せます。値をnoneにすると無効になります
  • 踏み台自身の設定(UserやIdentityFile)は、宛先ホストのブロックから引き継がれません。踏み台用のHost bastionブロックを別に書く必要があります
  • IdentitiesOnly yesを付けると、ssh-agentが多数の鍵を持っていても、configで指定した鍵だけを提示します

Host *のような広い指定は末尾に置きます。configは各項目で最初に得られた値が使われるため、広い指定を先頭に置くと、後ろの個別指定が効かなくなります。

解決結果はssh -Gで確かめる

書いたconfigが意図どおりに解決されるかは、接続せずにssh -Gで確認できます。-Gは、HostとMatchを評価したあとの設定を表示して終了するオプションです。上のconfigを一時ファイルに置いて、OpenSSH 10.2p1で実行した結果が次です。

ssh -F ./cfg -G app-stg | grep -i -E '^(hostname|user|proxyjump|identityfile) '
user deploy
hostname 10.0.1.20
identityfile ~/.ssh/keys/id_ed25519_app
proxyjump bastion

踏み台側も同じ要領で見ます。bastionを指定すると、hostname bastion.example.comと踏み台用の鍵が出て、proxyjumpの行は出ませんでした。踏み台が踏み台を持たないことが、この出力から読み取れます。

この確認はClaudeに任せられます。ssh -Gは設定の解決結果を出すだけで、鍵ファイルの中身は出力に含まれません。「新しいHostブロックを足して、ssh -Gでproxyjumpとidentityfileを確認して」と頼めば、鍵を開かずに検証まで進められます。

鍵の中身をClaudeに読ませない

鍵を隠す層は2つあり、守る範囲が違います。

Readの拒否ルールで、ファイルツールを止める

permissions.denyにReadルールを書くと、Claudeのファイルツールからその鍵が読めなくなります。

{
  "permissions": {
    "deny": [
      "Read(~/.ssh/keys/**)"
    ]
  }
}

~/で始まるパスはホームディレクトリからの指定です。この拒否は、次の範囲に及びます。

  • Readの拒否は、同じパスへのEditとWriteも止めます。新しいファイルをそこに作ることも含みます
  • cat、head、tail、sed、teeなど、Claude Codeが認識するファイル系のBashコマンドと、<などのリダイレクト先にも適用されます
  • 鍵へのシンボリックリンクも拒否されます。拒否ルールは、要求されたパスと解決先のどちらかが一致すれば効きます。./project/keyから~/.ssh/id_rsaへのリンクを使った例が、そのまま記載されています

一方で、効かない場面があります。grep -r pattern .のようにファイルを名指ししないコマンドや、PythonやNodeのスクリプトが自分でファイルを開く場合は対象外です。ファイルツールの拒否は、全プロセスを止める壁ではありません。

sandboxのcredentialsで、コマンド側も止める

OSレベルで止めたいなら、sandboxのcredentialsに鍵のディレクトリを入れます。

{
  "sandbox": {
    "enabled": true,
    "credentials": {
      "files": [
        { "path": "~/.ssh/keys", "mode": "deny" }
      ]
    }
  }
}

mode: "deny"のエントリは、sandbox内のコマンドからの読み取りを拒否します。filesystem.denyReadと同じ制限です。スクリプトが鍵を開こうとしても、sandbox内なら失敗します。

Readの拒否ルールは、Claude CodeがsandboxのdenyReadにも加えます。一方、sandboxのdenyReadだけでは、組み込みのReadツールは止まりません。両方を書くのは冗長ではなく、止める対象が違うからです。

鍵を隠したまま、sshは動かす

ここまで隠すと、疑問が出ます。Claudeが鍵を読めないなら、sshは動くのでしょうか。

動きます。sshが鍵を開くのは、Claudeではなくsshプロセスです。Readの拒否はファイルツールと認識されたコマンドを止めるだけなので、ssh app-stgの中での鍵の読み取りは止まりません。

ただし、sandboxを有効にしている場合は話が別です。素のsshはプロキシ変数を読まないため、sandbox内では許可済みのホストでも失敗します。経路の作り方はsandboxから繋がらない原因と回避にまとまっています。gitのSSHだけがmacOSで落ちる件はgitのSSH接続がmacOSのsandboxで失敗する件で扱っています。

sshをexcludedCommandsでsandboxの外に出すなら、代償を理解しておく必要があります。excludedCommandsに入れたコマンドは、ファイルシステムの制限もネットワークプロキシも受けずにsandboxの外で動くため、allowedDomainsの制限も及びません。そこで、askルールで実行のたびに承認を挟む形が安全です。

{
  "permissions": {
    "ask": ["Bash(ssh *)", "Bash(scp *)"]
  },
  "sandbox": {
    "excludedCommands": ["ssh *", "scp *"]
  }
}

この組み合わせでは、sshは承認のうえでsandbox外で動き、鍵を読めます。それ以外のsandbox内のコマンドは、credentialsの拒否で鍵に触れません。

拒否ルールの文面にも限界があります。Bash(ssh *)は、Claudeが書いたsshのコマンド文に一致するルールです。/usr/bin/ssh ...やsh -c 'ssh ...'のような別の形は、同じルールでは止まりません。強制力が要る部分は、sandboxやPreToolUseフックで担保します。

承認を省きたい操作だけを限定する

毎回の承認が重いなら、読み取り専用の確認用コマンドだけをallowに回す手があります。ただし、permissionsのルールはdeny、ask、allowの順に評価され、最初に一致したルールで結果が決まります。具体的なルールでも順序は変わらないため、前の節のaskにBash(ssh *)を置いたままだと、allowに足したルールも毎回確認に回ります。承認を省くなら、askのほうを本番に限ったルールへ狭めます。

{
  "permissions": {
    "ask": ["Bash(ssh app-prod *)", "Bash(scp *)"],
    "allow": ["Bash(ssh app-stg uptime)", "Bash(ssh -G *)"]
  },
  "sandbox": {
    "excludedCommands": ["ssh *", "scp *"]
  }
}

この構成では、ssh app-prodで始まるコマンドは常に確認を挟み、ssh app-stg uptimeとssh -Gは確認なしで通ります。どちらのルールにも一致しないssh(たとえばssh app-stg rm -rf /tmp/x)は、ルールでは決まらず、そのときの権限モードの扱いになります。*を含まないルールは、そのコマンドと完全に一致したときだけ通ります。ssh app-stg uptimeは通り、ssh app-stg uptime --fooは通りません。許可を広げるときは、ssh app-*のようにホスト側でなく、実行するコマンドの側で絞るほうが、意図しない接続先を許しにくくなります。

踏みやすい点

落とし穴

踏み台のブロックを書かず、鍵が合わない

ProxyJump bastionと書いただけで、宛先のUserやIdentityFileが踏み台にも使われると思いがちです。実際には、宛先の設定は踏み台に適用されません。踏み台専用のHost bastionブロックに、UserとIdentityFileを書きます。

落とし穴

IdentitiesOnlyを外すと、agentの鍵を順に試す

IdentitiesOnlyがno(既定)のままだと、ssh-agentが持つ鍵もすべて提示の候補に入ります。agentに多数の鍵があるときに、意図しない鍵まで提示されるのを避けるためのオプションです。ホストごとに鍵を決めたいときはIdentityFileとIdentitiesOnly yesをセットで書きます。

ForwardAgentは、踏み台にyesで付けないほうが無難です。転送されたagentに接続できる権限を持つ相手は、鍵そのものは取れなくても、agentに載っている鍵での認証操作はできてしまいます。Claudeに鍵を渡さない運用なら、踏み台の先でも鍵を使わせない設計に寄せるほうが一貫します。

パスフレーズ付きの鍵を使う場合、sshがBashツールの中でパスフレーズを要求して画面が崩れる報告があります。ssh-agentに先に鍵を載せておく回避策はsshでpassphraseを聞かれて固まる原因と回避策にあります。

CLAUDE.mdに添える運用メモ

拒否ルールは強制力を持ちますが、Claudeが迷わず動くには、使ってよいホストの一覧も要ります。configを読ませたうえで、プロジェクトのCLAUDE.mdに次のような断片を置いておく方法があります。

## SSH
- 接続先は ~/.ssh/config の Host 名だけを使う(app-stg / app-prod)
- 新しい Host を足したら `ssh -G <host>` で proxyjump と identityfile を確認する
- 鍵のパス指定(-i)や ssh-add は使わない
- 本番(app-prod)への接続は、実行前に目的を1文で説明する

CLAUDE.mdは、Claudeの動き方を方向づけるメモです。禁止の強制は拒否ルールとsandboxが担い、メモは「どのホストをどう使うか」を揃える役に回すと、役割が混ざりません。

configが長くなる場合は、Includeで別ファイルに分けられます。複数のパス指定とワイルドカードに対応し、案件ごとにホスト定義を分けておくと、Claudeに見せる範囲も分けやすくなります。

設定をどの階層に置くか

ここまでのpermissionsとsandboxは、個人のユーザー設定に置くのが基本です。理由は、~/で始まるパスがホームからの指定で、どのプロジェクトでも同じ鍵を守れるからです。

/pathのように1つのスラッシュで始めたパスは、設定ファイルの位置を基準にします。ユーザー設定のRead(/secrets/**)は~/.claude/secrets/**を指し、プロジェクトのsecretsではありません。ホーム直下の鍵を守る目的なら~/か//の絶対パスを使います。

複数人のチームでホスト一覧を配る用途なら、Claude DesktopのSSH接続を配る仕組みもあります。sshConfigsでチーム共通のSSH接続を配る手順を参照してください。Claude Code自体をリモートで動かす設定はClaude Code SSH接続ガイドに、設定ファイル全体の位置付けはsettings.jsonの完全ガイドにあります。

まとめ

configには「どこへ、誰として、どの鍵のパスで」を書き、鍵の中身は~/.ssh/keys/に集めてReadの拒否とsandboxのcredentialsで隠す。sshの実行はaskで承認制にし、sandbox内で失敗するならexcludedCommandsで外に出す。この三段で、踏み台を含む接続を保ったまま、Claudeに渡る情報をホスト名とパスだけに絞れます。拒否ルールの文面は別の呼び方で迂回できるため、鍵を守る本体はsandboxのほうに置くのが筋です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/sandboxing
  3. code.claude.com/docs/en/settings-reference
  4. man.openbsd.org/ssh_config
  5. man.openbsd.org/ssh
この記事を共有:XはてブLinkedIn