Linuxでssh-agentとSSH鍵転送をマスターする:複数の鍵を管理し、サーバーを安全に連鎖させる

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

問題:鍵が多すぎる、プロンプトも多すぎる

Linuxサーバーを2台以上管理した経験があれば、この苦労はよくわかるはずだ。GitHubにSSH鍵が1つ、本番環境に1つ、ステージングに1つ、クライアントのVPSにもう1つ。接続のたびにパスフレーズを要求されるか、あるいは利便性のために全サーバーで同じ鍵を使い回して、何かが漏洩しないことを祈るかのどちらかだ。

エンジニアたちはそれぞれ異なるアプローチでこの問題に対処している。その違いは大きく、やみくもに選ぶと後で痛い目に遭うことが多い。

複数のSSH鍵を扱う4つの方法

アプローチ1:すべてに1つの鍵を使う

最もシンプルな構成——1つの鍵ペアをすべてのサーバーとサービスに使用する。設定不要なため、ほとんどの人はここから始める。

アプローチ2:SSH Configでホストごとに鍵を分ける

サーバーやサービスごとに別々の鍵ペアを作成し、~/.ssh/configの中でホストごとに使用する鍵を指定する:

Host github.com
    IdentityFile ~/.ssh/github_ed25519

Host production
    HostName 203.0.113.10
    User ubuntu
    IdentityFile ~/.ssh/prod_ed25519

アプローチ3:鍵をロードしたssh-agent

ssh-agentプロセスを起動し、鍵を一度だけロードして(パスフレーズもその1回だけ入力)、SSHがセッションの残り時間中に自動的に再利用する。

アプローチ4:ssh-agent + SSH鍵転送

最も高機能な構成。エージェントはローカルで動作する。bastionやジャンプホストにSSH接続すると、ローカルエージェントのソケットがそのマシンに転送される——これにより、秘密鍵がリモートのディスクに触れることなく、内部サーバーまで認証を連鎖させることができる。

メリットとデメリット

アプローチ1:すべてに1つの鍵を使う

  • メリット: 設定不要、すぐに使える
  • デメリット: 1つの鍵が漏洩すると、全サーバーへのアクセスが危険にさらされる
  • デメリット: 利便性のためパスフレーズを省略すると、ディスク上の秘密鍵が無防備になる

アプローチ2:SSH Configでホストごとに鍵を分ける

  • メリット: 分離性が高い——1つの鍵が漏れても、他の鍵は安全なまま
  • メリット: どの鍵がどこに使われるかが自明な、クリーンで読みやすい設定
  • デメリット: セッションごと、鍵ごとにパスフレーズを再入力する必要がある
  • デメリット: サーバーAからサーバーBにジャンプする際、Aに秘密鍵をコピーしなければ使えない

アプローチ3:ssh-agentのみ

  • メリット: パスフレーズを接続ごとではなく、セッションにつき1回だけ入力すればよい
  • メリット: 秘密鍵はディスク上で暗号化されたまま——エージェントはメモリ内にのみ保持する
  • デメリット: ターミナルを閉じるとエージェントが終了する(永続化しない限り)
  • デメリット: 中間マシンに鍵を置かない限り、サーバーのホップ連鎖には対応できない

アプローチ4:ssh-agent + SSH鍵転送

  • メリット: マルチホップのシナリオでも、秘密鍵がローカルマシンから出ることはない
  • メリット: ターミナル1つで全連鎖:ラップトップ → bastion → 内部サーバー → データベース
  • デメリット: bastionホストが信頼できることが前提——侵害されたrootユーザーが、転送されたソケットを一時的に悪用できる
  • デメリット: 他のアプローチよりも初期設定が少し多い

推奨セットアップ

数台のサーバーを管理するほとんどのエンジニアにとって、アプローチ4は設定時間に見合う価値がある。ホストごとの鍵をssh-agentにロードし、自分が管理する特定のbastionホストにのみ転送を有効にする。

アプローチ2(IdentityFileを使ったSSH config)を基盤として維持すること。その上にパスフレーズの利便性のためssh-agentを重ねる。エージェント転送は明示的に必要な場所だけに有効にし——グローバルに設定しないこと。

私が運用しているUbuntu 22.04サーバー(RAM 4GB)では、Ansibleでの違いはすぐに現れた。単一のbastionを通じて10台以上の内部ノードに接続するPlaybookは、以前はホストごとにパスフレーズを要求されるか、bastion上に暗号化されていない鍵を置く必要があった。エージェント転送を使えば、Playbookが無人で実行される。秘密鍵はリモートのファイルシステムに一切触れない。

実装ガイド

ステップ1:用途ごとに別々の鍵を生成する

Ed25519を使用する——ほとんどのユースケースでRSAより高速で暗号的に強固だ:

# GitHub用の鍵
ssh-keygen -t ed25519 -C "github" -f ~/.ssh/github_ed25519

# 本番サーバー用の鍵
ssh-keygen -t ed25519 -C "prod-servers" -f ~/.ssh/prod_ed25519

# クライアントVPS用の鍵
ssh-keygen -t ed25519 -C "client-vps" -f ~/.ssh/client_ed25519

プロンプトで要求されたときは必ず強力なパスフレーズを設定すること。設定しないと、ディスク上の鍵ファイルは盗まれるのを待つだけのテキストファイルになってしまう。

ステップ2:ssh-agentを起動する

GNOMEやKDEのデスクトップ環境では、通常はセッション開始時にSSHエージェントが自動起動される。まず確認しよう:

echo $SSH_AUTH_SOCK
# 例:/run/user/1000/keyring/ssh のような値が表示されるはず

変数が空の場合は、手動で起動する:

eval "$(ssh-agent -s)"
# Agent pid 12345

すべてのシェルセッションで自動起動するには、~/.bashrcまたは~/.zshrcに以下を追加する:

if [ -z "$SSH_AUTH_SOCK" ]; then
    eval "$(ssh-agent -s)"
fi

ステップ3:エージェントに鍵を追加する

ssh-add ~/.ssh/github_ed25519
# パスフレーズを入力:(一度だけ入力すれば、セッション中はずっと有効)

ssh-add ~/.ssh/prod_ed25519
ssh-add ~/.ssh/client_ed25519

# ロードされた鍵を確認
ssh-add -l
# 256 SHA256:abc123... github (ED25519)
# 256 SHA256:def456... prod-servers (ED25519)

共有マシンや信頼性の低いマシンでは、時間制限を追加する。指定した時間が経過すると鍵が自動的に削除される:

# 4時間後(14400秒)に自動削除
ssh-add -t 14400 ~/.ssh/prod_ed25519

ステップ4:~/.ssh/configを設定する

鍵をホストに紐付け、信頼できるbastionホストのみに転送を有効にする:

Host github.com
    IdentityFile ~/.ssh/github_ed25519
    AddKeysToAgent yes

Host bastion
    HostName 203.0.113.5
    User ubuntu
    IdentityFile ~/.ssh/prod_ed25519
    ForwardAgent yes        # この信頼できるホストのみ有効にする
    AddKeysToAgent yes

Host internal-db
    HostName 10.0.1.20
    User ubuntu
    ProxyJump bastion       # bastionを経由してアクセス — IdentityFileは不要

Host client-vps
    HostName 198.51.100.10
    User root
    IdentityFile ~/.ssh/client_ed25519
    AddKeysToAgent yes
    # ForwardAgentを意図的に省略 — 外部ホストには転送しない

ステップ5:連鎖をテストする

この設定により、ラップトップから内部データベースへの接続はコマンド1つで済む:

ssh internal-db

SSHが実際に行う処理は以下の通りだ:

  1. 転送されたエージェントを通じて、ローカルの本番鍵を使ってbastionに接続する
  2. bastionから10.0.1.20への2番目の接続を開く
  3. 内部サーバーが転送されたローカルエージェントに対して認証を行う
  4. 秘密鍵ファイルがbastionのディスクに触れることはない

リモート側で転送がアクティブになっていることを確認するには:

# bastionに接続後:
echo $SSH_AUTH_SOCK
# /tmp/ssh-XXXXXX/agent.NNNN  ← 転送されたソケットが存在する

ssh-add -l
# ローカルマシンの鍵を一覧表示

ステップ6:ターミナルセッションをまたいでエージェントを永続化する

もう1つの煩わしさ:ターミナルを閉じるとエージェントが終了し、ロードされた鍵も失われる。keychainユーティリティは、セッションをまたいで単一のエージェントを維持することでこの問題を解決する:

sudo apt install keychain

# ~/.bashrcに追加:
eval "$(keychain --eval --quiet ~/.ssh/prod_ed25519 ~/.ssh/github_ed25519)"

再起動後の最初のログイン時に一度だけパスフレーズを求められる。その後に開くすべてのターミナル——新しいタブ、再接続、tmuxペイン——は同じ実行中のエージェントを再利用する。もうプロンプトは表示されない。

本番投入前のセキュリティチェックリスト

  • 完全にコントロールできるbastionホストにのみForwardAgent yesを設定する
  • ワイルドカードHost *ブロックにForwardAgent yesを絶対に追加しない
  • 共有マシンにロードする鍵にはssh-add -t <秒数>を使用する
  • 機密性の高い操作の前にssh-add -lで現在ロードされている鍵を確認する
  • bastionの/tmpパーミッションを制限し、rootのSSHログインを無効にする

エージェント転送の実際のリスクは鍵のコピーではない——秘密鍵のバイトは本当にローカルに留まる。露呈するのはソケット自体だ。bastionのrootユーザーは、SSHセッションが開いている間、一時的にそのソケットを使用できる。bastionホストは自分のラップトップと同じ管理水準で扱うこと。

一度設定すれば、認証について考える必要がなくなる。ssh internal-dbと入力するだけで接続できる。鍵は本来あるべきローカルマシンに留まる。

Share: