あらゆるVPSまたはクラウドプロバイダーで初回SSH接続のMITMを防ぐ
(joachimschipper.nl)- ssh-init-vm は、新しいVMへの初回SSH接続で 中間者攻撃 を防ぐため、cloud-init で一時的なSSHホスト秘密鍵を注入し、長期ホスト鍵を生成・取得している間だけそれを信頼する
- Hetzner Cloud のように専用の接続保護機能がないVPSやクラウドでも動作し、必要なのは広くサポートされている cloud-init だけ
- 一般的な Trust On First Use では、
sshの “The authenticity of host [...] can't be established” という質問にyesを入力すると、攻撃者がトラフィックをプロキシしたり、ユーザーのVMに見せかけたマシンを提供したりできる - 長期SSHホスト秘密鍵を cloud-init userdata に直接入れると、初回接続の認証には役立つが、メタデータサービス・SSRF・クラウドプロバイダーのシステム・管理者ワークステーションを通じて 機密性の高い鍵情報 が露出する可能性がある
- ssh-init-vm は一時鍵を一時ディレクトリに置き、
~/.ssh/known_hostsには入れず、VMの出力をそのまま保存せずに OpenSSH の ホスト鍵ローテーション に依存して長期鍵を記録する
cloud-init userdata の露出問題
- cloud-init で長期SSHホスト秘密鍵を注入すれば、公開鍵を
~/.ssh/known_hostsに入れて初回接続を認証できるが、秘密鍵が複数の経路で漏えいする可能性がある - VM内部の任意のプロセスが、通常読み取れる メタデータサービス から userdata を取得できることがあり、Hetzner VM では
http://169.254.169.254/hetzner/v1/userdataで cloud-init の内容が見える場合がある - 攻撃者は SSRF によってプロセスにメタデータを漏えいさせることができ、この種の遮断は専用保護機能がある環境でも 常に適用されるとは限らない
- クラウドプロバイダーの他のシステムでも userdata が露出する可能性があり、Hetzner はサーバー作成APIの文書で “passwords or other sensitive information” を保存しないよう明記 している
- 管理者ワークステーションも cloud-init userdata が残ったり通過したりする場所になりうるため、長期秘密鍵を入れる方式は、その鍵が有効な間ずっと露出リスクを生む
セキュリティ分析と脅威モデル
- 前提は OpenSSH のプロトコルと実装 を信頼し、管理者が攻撃を検知できる能力には依存しないこと
-
ネットワーク攻撃者に対する保護
- 保護対象は管理者ワークステーションの完全性とVM
- 攻撃者はネットワークを完全に制御する中間者であり、スクリプトが成功または失敗して終了した後のどこかの時点で cloud-init userdata を知りうる
- 攻撃者は価値のある時点で鍵情報を知らないため、保護が成立する
- スクリプト は、一時SSHホスト鍵の偶発的な使用を防ぐためにそれを一時ディレクトリに保管し、一時SSHホスト鍵を
~/.ssh/known_hostsに入れない
-
管理者ワークステーションが侵害された場合
- 保護対象はVMとVMの長期SSHホスト秘密鍵に限定される
- 攻撃者はネットワークと管理者ワークステーションを完全に制御するが、実際のVMには接続しないと仮定する
- 長期SSHホスト秘密鍵は管理者ワークステーションに存在したことがなく、攻撃者も実際のVMに接続しないため、VMの長期ホスト鍵を取得できない
- 攻撃者が実際のVMに接続できれば、
ssh root@<VM> cat /etc/ssh/ssh_host_*のような方法でSSHホスト鍵を知れる可能性が高い
-
VMまたはプロバイダーが侵害された場合
- 保護対象は管理者ワークステーションの完全性に限られる
- 攻撃者はネットワークを完全に制御し、VMまたはプロバイダーも完全に制御できる
- この場合でも、OpenSSH が安全であるという前提により、管理者ワークステーションの完全性は保護される
- 追加防御として、スクリプト は VM の出力をそのまま
~/.ssh/known_hostsに書き込まず、OpenSSH の 鍵 ローテーション に依存して長期SSHホスト鍵を追加する - この方式により、侵害されたホストが
known_hostsパーサーに悪意あるデータを食わせることを防ぎ、VMが実際に 制御している鍵 だけが~/.ssh/known_hostsに記録される HashKnownHostsのような OpenSSH オプションや、将来の関連オプションも正しく処理できる
実際に中間者攻撃が成立する条件
- 中間者攻撃が成功するかどうかは、ユーザーが最初からすべての接続先が誤ったマシンだったと実際に気づくか、パスワード入力を拒否するか、
sshの agent または X11 転送を設定するかに左右される - ssh-mitm に基づく単純化した条件では、攻撃者が本物の対象ホストではなく攻撃者制御のマシンへのアクセス権を提供してユーザーを欺けるなら、成功する可能性が高い
- 攻撃者がユーザーをだまして本物のホストにログインできる情報を得られれば成功する
- ユーザーが攻撃者のマシンに パスワード でログインすると、攻撃者は成功できる
- ユーザーが何らかの認証方式でログインした後、プロンプトでパスワードを入力すると、攻撃者は成功できる
- ユーザーが何らかの認証方式でログインし、ssh-agent 接続 を転送すると、攻撃者は成功できる
- これらの条件がなければ、攻撃者はユーザーをだますために本物のホストへのアクセスが必要になるが、ユーザーの入力だけでは本物のホストにログインできず、失敗する可能性が高い
- ユーザーが X11 接続を転送すると、攻撃者は認証方式に関係なく管理者ワークステーションへの攻撃にも成功する可能性がある
1件のコメント
Lobste.rsのコメント
自動化できるのがすばらしい。手動では、クラウドプロバイダーのコンソールでサーバーのSSHフィンガープリントを別チャネルで確認していた
管理しているクラウドインスタンスはそれほど多くないので、プロビジョニングに手動の手順がいくつかあっても問題ない
DNSゾーンを自動化しているなら、別のアプローチも可能: 非常に限定された使い捨てトークンを作り、たとえば
my-server-hostname.example.netにレコードを1つ作成することだけを許可するそのトークンを
cloud-initでサーバーに渡し、サーバーが公開SSH鍵をDNSのSSHFPレコードとして登録する。その後、SSHクライアントにSSHFPレコードを自動検証させることができ、DNSゾーンはDNSSEC署名されている必要があるこの流れなら、サーバーは秘密SSHホスト鍵を保持したまま、鍵ローテーションを避けられる。ほとんどのDNSプロバイダーはこのような細かい使い捨てアクセストークンをサポートしていないが、トークンを検証したあと、永続的でスコープ制限のないトークンを使って代わりにAPI呼び出しを行う簡単な社内Webサービスを置くことはできる。SSHサーバーはその永続トークンにはアクセスできない
ただ、DNSSECドメインに書き込むよりはSSH証明書を生成するほうを好みそうで、そこから先はどの解決策が特定の環境により適しているかの問題になる
こうした柔軟なトークンを生成できるソフトウェアやプロバイダーを知っているのか、あるいはある程度自前で開発する必要があるのか気になる
かなりきれいだ。ただ、記事の日付で月と日が入れ替わっているように見える
以前、同じSSHの鶏と卵問題を探るために実験的なサービスを作ったことがある
リクエスト時にSSHFP DNSレコードを作成してくれるもので、興味があれば https://github.com/tedb/sshfp をどうぞ
VMプロバイダー間である程度一貫した169.254.169.254 メタデータサービスを扱ってくれているのはうれしい。
cloud-initソースの各種cloudinit/source/DataSource*.pyを見ると確認できる個人的には、
cloud-initの設計と限界のせいでだんだん疲れを感じている。ローカルのQEMU仮想マシン、リモートマシン、コンテナ、物理ハードウェア全体でシステム設定を統一することに関心があるarch-boxes project は ArchLinux の cloud-init image がどう作られているかを示しており、非常に単純なシェルスクリプトの集まりだ。この方法を
guestfishや µvm で応用すれば、OCI互換イメージ、QEMUやクラウドプロバイダー向けのディスクイメージ、新しい物理マシンのプロビジョニングにまったく同じスクリプトを使えるいくつかのQEMUフラグを組み合わせれば、
cloud-initへの依存なしに同じアプローチを再現できる。知る限り、systemd.system-credentialsでは一時的なホスト鍵を渡すことはできず、ssh.authorized_keys.rootのような~/.ssh/authorized_keys向けの認証情報しかない代わりに、initrd段階で実行される、あるいは
systemd-firstboot.serviceと一緒に実行されるunitファイルを作成できる。このunitファイルはイメージにあらかじめ入れておくか、systemd.extra-unit.*認証情報で一時注入し、systemd.wants=…カーネルコマンドラインオプションで有効化できる。QEMUでは-netdev user,id=metadata,net=169.254.0.0/16,dhcpstart=169.254.0.15,guestfwd=tcp:169.254.169.254:80-cmd:…でメタデータサービスの存在をエミュレートできる。ただし生成されたインターフェースを有効化する必要がある可能性が高く、これも一時的なunitファイルで処理したほうがよいかもしれないこうすることで、複数種類の「マシン」で一貫したシステム設定を行う際に、比較的低い複雑さでかなりの柔軟性が得られる。実際、この作業だけを見るなら、イメージ生成ツールが固定ホスト鍵入りのマシンイメージを作り、初回再起動またはシャットダウン時に実行されるカスタムのホスト鍵ローテーションスクリプトをSystemDサービスとしてインストールする方式が最良に思える
/etc/mkinitcpio.confでsystemdHOOKを有効にすると、initrd で実行するためのSystemD unitファイルを書けるし、実質そうすべきだ実際に使ってみると、
{/etc,/usr/lib}/initcpio/hooksを書くよりほんの少し面倒なくらいだしかし initrd で
systemd-networkingとsystemd-resolvedを有効にするのはかなり簡単なので、initrd がシステム起動の責任を持ち、ルートファイルシステムへ切り替わる前に作業を予約できるもちろん、ノートPCのような物理ハードウェアでは Wi‑Fi 接続に
NetworkManagerのようなものが必要になり、あまり適さないかもしれないが、QEMU VM やホスティングVMにはよく合い、多くのシステム起動処理がこの領域に自然に収まる目標は、
cloud-initに依存せず、特定のクラウドプロバイダー1社に縛られず、物理マシン・コンテナ・ローカルVM・ホスティングVM全体で一貫性を得て、依存関係を事実上SystemD程度まで減らすことだ