SSHの極めて詳細なガイド(ただし自分にとって役立つものだけ)
(grahamhelton.com)- SSHはリモート接続にとどまらず、ポートフォワーディング、ジャンプホスト、設定ファイル、鍵管理までをまとめて扱うツールであり、Webサーバーの例は RDP・SQLのような他のサービス にもそのまま応用できる
- ローカル・リモート・動的ポートフォワーディング はそれぞれ
-L、-R、-Dで構成され、どちら側でポートが開き、トラフィックがどこへ流れるかが核心的な違いである - 直接アクセスが遮断されたネットワークでは、
-Jジャンプホスト とProxyJumpによって複数のSSH経由地をつなぎ、目的地まで入ることができる ssh-agentと-Aエージェントフォワーディング は、リモートホストでもローカル鍵を使えるようにして便利だが、エージェントの悪用によるセキュリティリスクを先に確認する必要がある~/.ssh/config、ssh-copy-id、ssh-keygen、SSHコンソールの~?・~Cを併用すると、繰り返しのオプション入力を減らし、セッション中のフォワーディング追加、公開鍵の配布、鍵の生成・検査が容易になる
SSHポートフォワーディングを理解するための前提
- SSHポートフォワーディングは図だけでは流れをつかみにくく、実際のコマンドとネットワークシナリオを一緒に見ると理解しやすい
- 例はWebサーバーへのアクセスを基準にしているが、同じ方法は RDP、SQLなどほぼすべてのサービス に適用できる
- 繰り返し使うオプションは次の意味を持つ
-N: リモートサーバーでコマンドを実行せず、シェルも開かない-f: SSHをバックグラウンドに送るroot@host: トンネルを作成するユーザーとホストとしてログインする
ローカルポートフォワーディング -L
- ローカルポートフォワーディング は、ローカルマシンのポートをリモートサーバーのポートへ転送する
- 例の状況
internal-web.intがループバックインターフェースからのみアクセス可能なWebページをホスティングしているcampfire.intからinternal-web.intへSSHアクセスできるcampfire.intのローカルポート経由でinternal-web.intのWebサーバーにアクセスしたい
- 使用コマンド
ssh -N -f -L 1337:127.0.0.1:80 root@internal-web.int
- オプションの解釈
-L: ローカルフォワーディングを指定する1337:127.0.0.1:80: ローカルポート1337をリモートの127.0.0.1:80にバインドする
- トンネル作成後は、
campfire.intからローカルポート1337にリクエストを送ることで、internal-web.intのポート80とやり取りできる - 覚えておくべき点は、
-Lではローカルポートがアドレスの左側 に来ることだ
リモートポートフォワーディング -R
- リモートポートフォワーディング は、ローカルからアクセス可能なポートをリモートサーバー上のポートとして公開する
- 例の状況
internal-web.intはループバックからのみアクセス可能なWebページをホスティングしているcampfire.intはファイアウォールのためinternal-web.intに直接アクセスできないvuln-server.intはcampfire.intとinternal-web.intの両方からアクセス可能である
- 使用コマンド
ssh -N -f -R 3000:127.0.0.1:80 root@vuln-server.int
- オプションの解釈
-R: リモートフォワーディングを指定する3000:127.0.0.1:80:vuln-server.intのポート3000をローカルの127.0.0.1:80にバインドする
- その後
vuln-server.int:3000にcurlリクエストを送ると、internal-web.intのポート80で動作する内部Webページにアクセスできる - 覚えておくべき点は、
-Rではローカルポートがアドレスの右側 に来ることだ
動的ポートフォワーディング -D と SOCKSプロキシ
- 動的ポートフォワーディング は
-Dオプションで SOCKSプロキシを作り、トラフィックをSSH経由地へ送る - 例の状況
internal-web.intは内部ネットワークからのみアクセス可能なWebアプリケーションをホスティングしているvuln-server.intは同じ内部ネットワーク上にあり、internal-web.intにアクセスできるcampfire.intからvuln-server.int経由でトラフィックをプロキシしたい
/etc/proxychains.confの設定はSSHコマンドのポートと一致している必要があるsocks5: proxychains に SOCKS5 を使わせる127.0.0.1: localhost を使う8080: SSH の-Dに指定したポートと一致していなければならない
- 使用コマンド
ssh -N -f -D 8080 root@vuln-server.int
- フォワーディング作成後に
socks5 127.0.0.1 8080を設定すれば、proxychains curl 192.168.1.185で内部Webページにアクセスできる - SOCKS経由のDNSは環境によってうまく動かないことがあるため、例では ホスト名の代わりにIPアドレス を使っている
- Firefoxでも手動プロキシ設定で SOCKSプロキシを使える
- パス: Settings → Privacy & Security → Network Settings
- Manual proxy configuration を選択
- “Proxy DNS when using SOCKS V5” にチェックを入れる
- SOCKS host を
127.0.0.1、port を8080に設定する
ジャンプホスト -J
- ジャンプホスト は、現在のホストから直接アクセスできない宛先に対し、複数のSSH経由地を通って接続できるようにする
- 例のチェーンは
campfire.int→vuln-server.int→internal-web.int→dns.intである - 使用コマンド
ssh -J root@vuln-server.int,root@internal-web.int root@dns.int
- 複数のジャンプ先は カンマ で区切る
エージェントフォワーディング -A
ssh-agentは、ローカルマシンでssh-add <private_key_file>により秘密鍵やIDを追加できるようにする- 追加された鍵は
ssh-add -lで確認できる ssh-agentに鍵を追加すると、パスワードを再入力せずにその鍵でSSH接続できるため、人のアカウントにもサービスアカウントにも有用である-Aエージェントフォワーディング は、接続先のリモートマシンでもローカルエージェント内の鍵を使えるようにする- 使用前にセキュリティリスクを確認するには Zero Effort Private Key Compromise: Abusing SSH-Agent for Lateral Movement を参照する必要がある
- 例のコマンド
ssh -A -J root@vuln-server.int root@internal-web.int
- このコマンドは
vuln-server.intを経由してinternal-web.intに接続しつつ、ローカルのcampfire.intのSSHエージェントにある鍵を使えるようにする - その後
internal-web.intでssh root@dns.intを実行する際、秘密鍵の指定や認証情報の入力なしで接続できる
TTYコマンド割り当て -t
-tオプション は、リモートサーバーで対話型操作が必要なコマンドを素早く実行したいときに有用である- 例は
VimやtopのようにTTYが必要なコマンドである - 使用コマンド
ssh root@internal-web.int -t top
- 実行すると、リモートサーバー上で
topコマンドを含むTTYを受け取る
外部ホストもローカルフォワーディングポートに接続できるようにする -g
-gオプション は、リモートホストがローカルフォワーディングされたポートに接続できるようにする- ローカルポートフォワーディング
-Lと似ているが、外部マシンからもその「ローカル」ポートを使えるようにする点が違う - 例の状況
vuln-server.intへのシェルアクセスがあるvuln-server.intのポート2222に入る接続をinternal-web.intのポート22へプロキシしたい
- 使用コマンド
ssh -N -f -g -L 2222:localhost:22 root@internal-web.int
- オプションの解釈
-g: リモートホストがローカルフォワーディングポートに接続できるようにする-L: ローカルフォワーディングを指定する
vuln-server.intのポート2222にSSH接続していても、実際のシェルはinternal-web.int上にある
SSHコンソール ~? とセッション中のフォワーディング
- SSHコンソール は、リモートシステムと直接やり取りせずにSSH自体を制御できる隠れた機能である
- シェルが壊れたときや、SSHセッション自体を制御する必要があるときに有用である
- ヘルプコンソールは
~?で開ける - 便利なオプション
~.: 現在のSSHセッションを終了する~C: SSHコンソールを開き、フォワーディングオプションを追加できる
- すでに通常の
sshコマンドでvuln-server.intに接続している状態でも、~Cを押して-D 8080を入力すれば、そのセッションを動的フォワーディングセッションのように使える campfire.intで/etc/proxychains.confがポート8080を使うよう設定されていれば、最初からssh -Dで始めたのと同じように proxychains を活用できる
SSH設定ファイル ~/.ssh/config
- SSH設定ファイル は
~/.ssh/configにあり、繰り返し入力するSSHオプションを保存して時間を節約できる - SSH接続時にこのファイルがパースされ、接続先に対応する
host設定があればそのオプションが使われる - コマンドライン引数は設定ファイルより優先される
- たとえば設定ファイルで
internal-web.intのユーザーをrootに指定していても、ssh graham@internal-web.intを実行すればgrahamでログインを試みる
- たとえば設定ファイルで
- 基本設定の例
# You can put comments with a `#` at the beginning of the line only.
host internal-web.int
User root
IdentityFile /home/smores/ssh_agent/internal-web-no-pw
Port 2222
ssh internal-web.int実行時の処理の流れ- コマンドラインの
internal-web.intと~/.ssh/configのhost internal-web.intをマッチさせる - マッチすれば、コマンドラインで指定していないオプションを設定ファイルから取得する
- マッチしなければ、コマンドラインで定義したオプションだけを使う
- コマンドラインの
よく使うSSH configキーワード
IdentityFile /path/to/private_key- ホストで使う秘密鍵を指定する
ssh -iと同じ役割を持つ
ForwardAgentssh -Aと同じ役割を持つ- エージェントフォワーディングを使う前に Zero Effort Private Key Compromise: Abusing SSH-Agent for Lateral Movement の確認が必要である
ProxyJump root@internal-web.int- トラフィックをプロキシするサーバーを指定する
-Jオプションと同じ役割を持つ- 例では、トラフィックがそのホストを経由することを示すために
vuln-server.intの認証を先に要求する
Match- 条件に応じてSSH configキーワードを適用する
- 例では
export | grep PROXYME=TRUEコマンドの終了コードが0なら、Matchブロック以下のProxyJumpを使う PROXYME環境変数がなければ通常のhost internal-web.intブロックだけを使うexport PROXYME=TRUEを設定したうえで同じssh internal-web.intを実行すると、vuln-server.intの認証を経てからinternal-web.intのシェルを受け取る
scpや一部のSSHベースユーティリティも通常はSSH configを利用できる- 自動では使わない環境では
-F ~/.ssh/configで明示できる
- 自動では使わない環境では
公開鍵のコピー ssh-copy-id
ssh-copy-idは、公開鍵をサーバーへ素早くアップロードする小さなユーティリティである- 使用コマンド
ssh-copy-id -i internal-web root@internal-web.int
- オプションの解釈
-i internal-web: サーバー認証に使う秘密鍵名を指定するroot@internal-web.int: 公開鍵をアップロードするサーバーを指定する
鍵の生成と検査 ssh-keygen
ssh-keygenは秘密鍵・公開鍵のペアを生成するユーティリティである- 一般的には、
-bオプションでより大きな鍵サイズを指定する方法が推奨される - 例の環境ではデフォルトの鍵サイズは
3072だった - デフォルトアルゴリズムはRSAだが、
-tフラグで別のアルゴリズムを指定できる- 例:
ssh-keygen -t ecdsa -b 521
- 例:
- 鍵のフィンガープリントとバイトサイズは次のコマンドで確認できる
ssh-keygen -lf <file-name>
1件のコメント
Hacker News のコメント
ここには驚くほど単純なディレクティブが1つ抜けている。
sshd_configでAuthorizedKeysCommand /usr/bin/php /etc/ssh/auth.php %uのように設定し、スクリプトでGitHubのhttps://github.com/{$user}.keysを取得すればよいもちろん本番品質のコードではないが、要点は示している
GitHubの組織/グループ所属を確認したうえで、ユーザーが存在し、
nss-atoのようなものでマッピングされていれば、サーバーへのログインを許可できる人をオンボーディング/オフボーディングするときに、GitHubグループへ追加/削除するだけで機器へのアクセスを付与または取り消せるので、手間が減る
Amazon Linux 2以前では、
authorized_keysファイル内の個々の鍵形式を確認するためにopensslコマンドラインを呼び出しているが、RSAにハードコードされていたため、OpenSSHのバージョンがed25519をサポートしていても、Amazon Linux 2ホストにはed25519で認証できなかった理論上はすばらしい機能¹を可能にするのでよいが、実際にはその機能を使わない人にとっても基本機能を壊してしまい、Amazon Linuxへの信頼を下げることになる
初めてこの問題に遭遇したのは、クラウドファーストなDevOpsの同僚が所有するマシンへSSH接続しようとしていたときで、直接触れなかったため診断が難しかった
彼はAWSには詳しいがLinuxにはそれほど詳しくなく、どこを見ればよいか分からなかった。クラウドプラットフォームの所有者が作ったディストリビューションだから「より互換性が高い」だろうと考えてAmazon Linuxを選んだのだが、ここでの「より互換性が高い」とは実際には「ばかげたサプライズがより多い」という意味だった
¹ https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-...
「ネットワークがちゃんと動かない」が職務範囲なので、こういう格好いい機能を見逃してしまう
OpenSSHの設定パーサーは重複したディレクティブを無視し、同じディレクティブのうち最初のものだけが有効になる、ということを知らない人は多そうだ
一般的な設定パーサーやルールエンジンでは、後から出てきたディレクティブが前のものを上書きすることが多いので、かなり直感に反する
些細に見えるかもしれないが、
/etc/ssh/sshd_configのようなデフォルト値を変えるとき、人やソフトウェアは変更内容をファイルやディレクティブブロックの末尾に追記しがちで、それが適用されると期待するセキュリティ会社や組織、さまざまなSSHバスチョン製品もここを間違えるし、CIS Benchmarksの推奨事項や大半のサードパーティ製CIS監査ツールも、優先順位を考慮しないか誤って扱っている
OpenSSHの設定ディレクティブが期待どおりに定義されているか確認するには、設定ファイルを直接眺めるのではなく、
sshd -Tやssh -Gで導出された内部設定をダンプすべきだsudoersファイルも同じ方式で動作するユーザー認証/認可ソフトウェアでは、この方式が望ましいと思う
whatever.conf.dディレクトリに新しいファイルを追加しても上書きできない設定を作りやすいからだメイン設定ファイルで
whatever.conf.d/*を読み込む前にその設定を定義し、そのファイルに特別な保護をかけておけばよい誰かが制御を迂回する状況でなくても、全体の文脈を知らない新人が新しいファイルを追加したり、何らかのパッケージが自分のサービス用の奇妙なデフォルト値をインストールしたりしても、基準設定が優先権を保つという点で設定管理に有利だ
別の文脈で逆の動作をより頻繁に見る理由は、欲しいものが「基準設定」ではなく、ユーザー/開発者/管理者が明示的に設定していないときに使われる厳密な意味でのデフォルト値だからだ
AllowUsersがそうだただ昨日、NixOSが突然マージ順序を変えたせいで、私のファイルの
AllowUsersが別ファイルのMatchの下に移動してロックされてしまったことがあった-Lはローカルフォワーディングでローカルがアドレスの左側にあり、-Rはリモートフォワーディングでローカルポートがアドレスの右側にある、と覚える説明が、この文章で最も重要かつ簡潔な一文だ最初から
-Lと-Rが混乱のもとで、どのポートインスタンスがローカルなのかがL/Rによって変わるのはかなり厄介だ-Lと-Rが意図する方向、つまり開始側と応答側がどこにあるかを変えるのは理解しているが、port:address:portは常にlocal:binding:remoteを意味するようにし、-L/-Rがどちら側で受信し、どちら側へ送信するかを決める形でもよかったのではないかと思う-Lは直後に出てくる番号のローカルポートで待ち受け、-Rは直後に出てくる番号のリモートポートで待ち受ける、と覚えるのがいちばん簡単だ残りの
host:portは、どこへ接続するかを示す一般的な形式にすぎないSSHトンネルを通じてポートフォワーディングを行うので、ホストは待ち受けポートがある側とはトンネルの反対側から接触される、という点も自然に導かれる
SSH には、あまり知られていないものの便利な機能として接続の多重化がある
新しい TCP 接続を作って認証手順をやり直す代わりに、既存の接続を再利用できる
プロトコル自体にチャネルの概念があり、各データフレームには異なるストリームを区別するメタデータが付いていて、この機能はそれを利用している
以後のセッションで完全な認証をやり直さなくて済むのが大きな利点
リモート側に
tmuxなどがなく、複数のターミナルウィンドウで複数のパネルを使う場合は特に便利で、認証にパスフレーズや HSM のタッチのような数秒かかる手順があると、より効果を実感できる「接続を維持」する設定もあり、数台のサーバーを行き来するたびに毎回認証しなくて済む
全体として、あればうれしいが人生が変わるほどではない機能だと思う
一部のサーバーではこの機能が無効になっているが、有効なときよりも無効なときのほうが、その不在をより強く感じる
詳細: https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing
SSH は往復回数を減らすよう最適化された TLS と違って、かなりおしゃべりなプロトコルであり、この多重化オプションはほぼ唯一の例外
ProxyJumpの踏み台ホストを経由するとき、Ansible がずっと使いやすくなる~/.ssh/configにホストが多いなら、ワイルドカード対応のIncludeディレクティブでファイルが散らかりすぎるのを防げるたとえば
~/.ssh/configにはInclude config.d/*.confを置き、~/.ssh/config.d/work.confやclient1.confにそれぞれhost、hostname、user設定を分けて置けるHostディレクティブもワイルドカードに対応しているたとえば
host *_workを追加し、host1_workのようなすべての仕事用ホストに共通する設定を入れられるIncludeをHost/Matchディレクティブの中に入れられるたとえば
~/.ssh/configにHost proj1.*.corpとInclude ~/.ssh/proj1.confを置けば、プロジェクトごとのマッチを上のほうの近い場所に置けて、レビュー時に大量の個別ファイルを探し回る必要も減るフォワーディングするときは、ほとんど
-fを使わないどのフォワーディングがまだ開いていて動作中なのか把握しにくくなり、自分の足を撃つ銃になりかねない
-tはすばらしい小技で、知らなかった機能だ~エスケープコマンド一覧で見落としやすい重要な点は、ネストしたセッション内でもエスケープをネストできること何らかの理由で
-Jを使わないときに便利一覧は有用なものとうまく合っていて、追加で学んだこともいくつかあった
-tはすばらしい。私はssh -t my-dev-vps 'tmux new-session -A -s main'のように使い、実行するたびにtmux セッションの以前の位置へすぐ戻っているプロセス一覧を自分で漁る以外に、SSH が妥当な形でフォワーディング状態を出してくれるとよいのだが
AF_UNIX対応を追加する現在のプルリクエストがあり、これが入れば、さまざまな興味深いフォワーディングが可能になるはず任意のローカルプロセス経由で SSH 接続をプロキシしやすくなり、そのプロセスがリモート端までデータを渡す処理を好きに扱えるようになるからだ
https://github.com/openssh/openssh-portable/pull/431
AF_UNIXを使う-Dだが、すべてがAF_UNIX上で動作可能になるのはよいこと約 1 年前から
curlは、ALL_PROXY構文socks5://localhost/pathまたはsocks5hを通じてAF_UNIXSOCKS を使えるようになっているようだTor が
AF_UNIXSOCKS プロキシを使うために追加されたものと思われる標準の Unix 権限でネットワークアクセスを設定できるとよいし、個人的には TCP/IP をカーネルから完全に追い出せるならさらによい
SSH コンソールを初めて見たときは衝撃的だった
同僚が
~#を見せてくれたのだが、SEGA Genesis のゲームに出てきそうな秘密のチートメニューを見つけたような気分だったなぜチルダなのか?
rlogin、rshが使っていたからなぜ
rlogin、rshはチルダを使っていたのか?cuが使っていたからなぜ
cuなのか? モデムやシリアル回線があるとcuで通信しており、Hayes コードを送る必要があったが、Hayes コードのエスケープシーケンスを使うとモデム側に抜けてしまうため、cuから抜けるための別の合図が必要だったなぜ
^[ではないのか? それは telnet だからつまり telnet でホストに接続してから
cuでモデムに接続した場合、telnet から抜けずにcuに戻るための別のエスケープ構文が必要だった結局、エスケープ構文が果てしなく積み重なった構造なのだ
そして実際にはチルダではなく、チルダである
CR、チルダ、.の順だと思っていたのだが、これまで間違って使っていたのだろうか?ssh-copy-idセクションは、コマンドが公開鍵をアップロードすると説明していたのに、突然秘密鍵をアップロードすると変わっており、誤字のように見えるまた、このコマンドは単に鍵をアップロードするだけでなく、
~/.ssh/authorized_keysに追記するので、はるかに便利最後に、
ssh-keygenセクションについては、最近読んだ資料ではecdsaよりed25519が好まれている