6 ポイント 投稿者 GN⁺ 2023-08-24 | 1件のコメント | WhatsAppで共有
  • SSHはリモート接続にとどまらず、ポートフォワーディング、ジャンプホスト、設定ファイル、鍵管理までをまとめて扱うツールであり、Webサーバーの例は RDP・SQLのような他のサービス にもそのまま応用できる
  • ローカル・リモート・動的ポートフォワーディング はそれぞれ -L-R-D で構成され、どちら側でポートが開き、トラフィックがどこへ流れるかが核心的な違いである
  • 直接アクセスが遮断されたネットワークでは、-J ジャンプホストProxyJump によって複数のSSH経由地をつなぎ、目的地まで入ることができる
  • ssh-agent-A エージェントフォワーディング は、リモートホストでもローカル鍵を使えるようにして便利だが、エージェントの悪用によるセキュリティリスクを先に確認する必要がある
  • ~/.ssh/configssh-copy-idssh-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.intcampfire.intinternal-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:3000curl リクエストを送ると、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.intvuln-server.intinternal-web.intdns.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.intssh root@dns.int を実行する際、秘密鍵の指定や認証情報の入力なしで接続できる

TTYコマンド割り当て -t

  • -t オプション は、リモートサーバーで対話型操作が必要なコマンドを素早く実行したいときに有用である
  • 例は Vimtop のように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/confighost internal-web.int をマッチさせる
    • マッチすれば、コマンドラインで指定していないオプションを設定ファイルから取得する
    • マッチしなければ、コマンドラインで定義したオプションだけを使う

よく使うSSH configキーワード

  • IdentityFile /path/to/private_key
    • ホストで使う秘密鍵を指定する
    • ssh -i と同じ役割を持つ
  • ForwardAgent
  • 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件のコメント

 
GN⁺ 2023-08-24
Hacker News のコメント
  • ここには驚くほど単純なディレクティブが1つ抜けている。sshd_configAuthorizedKeysCommand /usr/bin/php /etc/ssh/auth.php %uのように設定し、スクリプトでGitHubのhttps://github.com/{$user}.keysを取得すればよい
    もちろん本番品質のコードではないが、要点は示している
    GitHubの組織/グループ所属を確認したうえで、ユーザーが存在し、nss-atoのようなものでマッピングされていれば、サーバーへのログインを許可できる
    人をオンボーディング/オフボーディングするときに、GitHubグループへ追加/削除するだけで機器へのアクセスを付与または取り消せるので、手間が減る

    • Amazon Linuxも似たことをしているが、おそらく本番品質という理由でずっと複雑になっている
      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-...
    • こういうものを見ると、ネットワーク側にいるのが惜しくなる
      「ネットワークがちゃんと動かない」が職務範囲なので、こういう格好いい機能を見逃してしまう
    • こういう用途なら、代わりにSSH証明書を使うのがよい
  • OpenSSHの設定パーサーは重複したディレクティブを無視し、同じディレクティブのうち最初のものだけが有効になる、ということを知らない人は多そうだ
    一般的な設定パーサーやルールエンジンでは、後から出てきたディレクティブが前のものを上書きすることが多いので、かなり直感に反する
    些細に見えるかもしれないが、/etc/ssh/sshd_configのようなデフォルト値を変えるとき、人やソフトウェアは変更内容をファイルやディレクティブブロックの末尾に追記しがちで、それが適用されると期待する
    セキュリティ会社や組織、さまざまなSSHバスチョン製品もここを間違えるし、CIS Benchmarksの推奨事項や大半のサードパーティ製CIS監査ツールも、優先順位を考慮しないか誤って扱っている
    OpenSSHの設定ディレクティブが期待どおりに定義されているか確認するには、設定ファイルを直接眺めるのではなく、sshd -Tssh -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.confclient1.conf にそれぞれ hosthostnameuser 設定を分けて置ける

    • Host ディレクティブもワイルドカードに対応している
      たとえば host *_work を追加し、host1_work のようなすべての仕事用ホストに共通する設定を入れられる
    • 追加のコツとして、IncludeHost/Match ディレクティブの中に入れられる
      たとえば ~/.ssh/configHost proj1.*.corpInclude ~/.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_UNIX SOCKS を使えるようになっているようだ
      Tor が AF_UNIX SOCKS プロキシを使うために追加されたものと思われる
      標準の Unix 権限でネットワークアクセスを設定できるとよいし、個人的には TCP/IP をカーネルから完全に追い出せるならさらによい
  • SSH コンソールを初めて見たときは衝撃的だった
    同僚が ~# を見せてくれたのだが、SEGA Genesis のゲームに出てきそうな秘密のチートメニューを見つけたような気分だった

  • なぜチルダなのか? rloginrsh が使っていたから
    なぜ rloginrsh はチルダを使っていたのか? cu が使っていたから
    なぜ cu なのか? モデムやシリアル回線があると cu で通信しており、Hayes コードを送る必要があったが、Hayes コードのエスケープシーケンスを使うとモデム側に抜けてしまうため、cu から抜けるための別の合図が必要だった
    なぜ ^[ ではないのか? それは telnet だから
    つまり telnet でホストに接続してから cu でモデムに接続した場合、telnet から抜けずに cu に戻るための別のエスケープ構文が必要だった
    結局、エスケープ構文が果てしなく積み重なった構造なのだ
    そして実際にはチルダではなく、チルダである

    • CR、チルダ、. の順だと思っていたのだが、これまで間違って使っていたのだろうか?
  • ssh-copy-id セクションは、コマンドが公開鍵をアップロードすると説明していたのに、突然秘密鍵をアップロードすると変わっており、誤字のように見える
    また、このコマンドは単に鍵をアップロードするだけでなく、~/.ssh/authorized_keys に追記するので、はるかに便利
    最後に、ssh-keygen セクションについては、最近読んだ資料では ecdsa より ed25519 が好まれている