ポートノッキング批判

自分のLinuxシステムにおいてリモートからのファイルの読み出しを可能にしたいと仮定しよう。ポート22上でSSHサーバを実行するという「標準的」方法に不十分な点があることは広く知られている。

OpenSSHは大多数のLinuxシステムにインストールされているSSHサーバだが、バッファオーバフローやその他の脆弱性を悪用した攻撃を繰り返し受けているため、常に最新のパッチを適用し続けることは時間的に困難な場合が多く、その労力も割に合わないと感じられて、結局、ファイルにアクセスできない状態で我慢することになりがちだ。そのような場合には、ファイルアクセスを可能にする手段としてポートノッキングが役立つように思われるかもしれない。だが、過大な期待は禁物だ。

ポートノッキングは 「閉じられたポートを通じてメッセージを伝達する」手段である。その動作のあらましは次のとおりだ。初期状態ではサーバ上のすべてのポートはファイアウォールによってブロックされている。クライアントはさまざまなポートに一連の接続要求(ノック)を発行するが、ポートはブロックされているので、これらの要求は当然、拒否される。しかし、サーバ上では接続要求のログファイルを監視するデーモンが動作しており、接続要求の順序を暗号化されたコードとして利用する。デーモンが有意なコードを認識すると、(ノックの順序というコードで指定された特定のIPアドレスに対して、指定された特定のポート上で)SSHやその他のサービスが有効化される。

ポートノッキングは追加的なセキュリティ層として安全性を一段高める役割を果たす。また、ポートノッキングには秘匿性のメリットがあり、攻撃者はポートノッキングが利用されているかどうか検知することができない、いう主張も聞かれる。

実際の有効性は?

まず秘匿性の議論から片づけよう。ポートノッキングが広く使われるようになると仮定し(使われなければ存在する意味がないだろう?)、仮に全サーバの10%で動作していると考えてみよう。そうなれば、攻撃者はとにかく標的のマシンがポートノッキングを使用しているものと想定して攻撃にかかるだろう。攻撃が成功する確率は(秘匿性が用いられていないと仮定した場合の)10分の1だが、攻撃のコストは攻撃が成功した場合に期待できる利得に比べてはるかに小さいため、攻撃者にとっては十分に割に合う確率である。この種の手法はポートスキャニングではごく一般的なものだ。クラッカたちは自動化された攻撃方法を複数併用しており、各手段が成功する確率はわずかにすぎないが、すべてを合わせた総合力は相当に強力なものとなる。

ポートノッキングの秘匿性は有効範囲が限られており、標的にできそうもない面白味のないマシンに見せる効果があるのは、IPの全範囲をスキャンしながら開いているポートを見つけようとする攻撃者に対してである。より執念深い攻撃者なら、パケットストリームを傍受して一連のノックの(非常に特徴的な!)パターンを検出することや、あるサービスをホストしていることがわかっているのにポートが1つも開かれていないマシンを見つける、関係者などから人づてにそれとなく情報を入手する、といった多数の方法で標的のマシンがポートノッキングを利用しているかどうかを推察することができるだろう。ユーザのウェブログの目立たない場所に「本日、ポートノッキングの運用開始! イェー!」といった軽率な書き込みがあるのを見られた、という愚かな理由で苦もなく見破られてしまうケースさえ考えられる。

隠蔽によるセキュリティは、唯一の防御として用いるには不適当である。このことは理論的に一般に通じる真実であり、隠蔽の層を1つ追加する目的でシステムの複雑さを増すことには常にコストが伴う。ポートノッキングの場合、秘匿性を実現するために乗り越えなければならない困難は、結果的に得られるわずかなメリットに比べてはるかに大きい。しかも、後述するように、秘匿性を実現するには、もっとずっと簡便な方法がいくつも存在する。

ひとたび秘匿性を考慮から外せば、ポートスキャニングの本当の姿が明瞭に見えてくる。ポートは物理的な物体ではなく、単なる16ビットの整数にすぎない。したがって、一連のノックはアクセスを許可するための単純な一続きのビット列であり、言い換えれば1つのパスフレーズである。つまり、ポートノッキングは自らをパスフレーズで認証する1つの方法にすぎない。そこで当然問われるべきことは、ネットワーク越しにパスフレーズを送る方法として、この込み入った非効率なやり方は、パスフレーズをパケットに入れて送信する単純明快な方法と比べてどのような点が優れているのか、である。すぐわかるように、優れていない、がその答えだ。(もう1つ即座に頭に浮かぶ問いは、ポートノッキングはSSHを使用したパスフレーズ認証とどう違うのか、という問題だが、それについては後で取り上げる。)

セキュリティをタマネギのようなものと考えてみよう。設計と実装が適切な場合、各層は攻撃者にとってまた1つ乗り越えねばならない新たなハードルを意味する。この考え方は「多層防御」と呼ばれている。だが、タマネギ構造の採用は、システムのセキュリティ分析が格段に難しくなるという代償を伴う。階層によってかえって見通しが悪くなり、セキュリティが確保できているという錯覚のもとで、実は1つの弱点が攻撃者に晒されたままになっているケースはあまりにも多い。

この罠に陥る危険を回避するには、タマネギの構築に標準的なコンポーネントを使用し、その組み立て方にも標準的な方法を採用することだ。逆に、ポートノッキングは、あらゆる面において、暗号の基本要素を不明瞭な形で実装した見本のようなやり方である。鍵を使っているなら、それは鍵と呼ぶべきだ。それをポート番号の並びという別の名で呼んでみても、得られるものが何もないどころか、むしろ弊害が大変大きい。鍵を鍵として考えれば、それを公共の場に保管すべきでないことは明らかだ。一方、(ポート番号の順序列という形で鍵を含んでいる)ログファイルは、非常に長期にわたって放置されたまま残る可能性がある。これは非標準的な用語法や実装がシステムのセキュリティ分析を複雑化させる典型例である。

ポートノッキングを標的とした攻撃を阻止できる可能性があるのは次の2つの理由による。1つはノックの順序列では(ユーザがカスタマイズ可能な)一部のポートしか使用を許可されないことであり、もう1つはノックの順序列をパスワードを用いて暗号化できることである。最初の防御は極めて貧弱なものだ。セキュリティは鍵の知識にのみ基づくべきであり、システム全体にばらまかれた細かな断片の中に置くべきではない。このことはKerckhoffsの法則として知られ、隠蔽によるセキュリティを戒めるスローガンとして使われることが多い。

2番目の防御は最初のものよりは格段に優れているが、驚くべきことに、ポートノッキングの概要が紹介されているページでは、この方法はオプションとして扱われている。実際のところ、ノックの順序列を暗号化をしない場合、ポートノッキングのセキュリティは皆無に近い。そして暗号化を使用した場合でも、いくつか問題がある。

第1の問題は、一部のポートしか有効でないという不要な「保護層」による複雑化である。仮に有効ポートとして32個のポート(現在の実装では最大256個まで使用可能)を選択したとしよう。ポートのノック順序列の長さはどの程度必要だろうか? 各ポートは16ビットの整数だから、8×16で(事実上解読不可能な)128ビットのセキュリティを得るために8回のノックが必要と考えるかもしれない。だが、各ポートが取り得る値は32通り(5ビット)のみであるため、実際には8×5=(わけなく解読可能な)40ビットのセキュリティしか得られない! もちろん、これはエンドユーザではなく実装者の問題であり、また、現在の(概念実証)実装ではこの長さの問題への対処がなされているようだ。しかし、間違いは容易に起こり得る。これが特に重大な問題であるのは、一連のポートノックが完了するまでに1分程度の時間を要する場合が往々にしてあり、実装者に対してノック順序列を可能な限り短くしてほしいという要求が高まることは必至であるからだ。2つの相反する目標の間で妥協を図ろうとすると、結果的にセキュリティの不備を招くことになる。

第2は、リプレイ攻撃にどう対処するかという問題である。リプレイ攻撃とは、クライアントとサーバの間に設置したルータでクライアントからの送信を傍受し、しばらく時間がたってからそれと同一のビットストリームをサーバに送ることによってクライアントに成りすます方法である。標準的な防御法はチャレンジ応答プロトコルだが、クライアントが認証を示す前にはサーバから何も送信しないことが望ましいので、このプロトコルは当然、検討の対象外だ。ドキュメントによると、ノックの順序列にはフラグフィールドが追加されており、ポートノックのたびにフラグの値を累加することでリプレイ攻撃を防ぐという。だが、これは非標準的な方法であり、有効性は疑わしい。サーバが再起動されたらどうなるのか? カウンタがリセットされた場合は? 私が調べた限りでは、フラグに割り当てられるフィールドは1バイトにすぎず、実際にリセットが発生するおそれは十分にある。

全般的に見て、ポートノッキングには攻撃の標的になり得る潜在的な弱点が多すぎる。特に重大なのは、サーバへの非ルートのアクセス権を持つユーザなら、誰でもたやすくシステムを破れる可能性があることだ。だが、開発者たちはこのことを懸念すべき問題とは捉えていないように思える。

ポートノッキングからの教訓

ポートノッキングからはセキュリティに関するいくつかの教訓を得ることができる。余計な付加機能がなく、保守の手間のかからない必要最小限の認証システムに対する需要は確かに存在する。OpenSSHの問題点は、何もかも実現しようとしていることだ。必要最小限のシステムを設計する際は、ターゲットとするユーザ層を常に念頭に置きながら、提供する機能や防御対象として想定する攻撃の範囲を決定することが不可欠である。私は、ポートノッキングの実装が、とかくバッファオーバフローのバグが付いて回るC言語で書かれていないという事実は好ましく思っている。ポートノッキングをSSHの代替として使うのではなく、SSHの一段上の層として追加するアプローチは、間違いなく堅実な方法である。一方、暗号の手法を独自に作り直すようなやり方は断じて適切なものとは言えない。

そこで、ポートノッキングを現在のポートノッキングの実装から切り離して、その本質を考えてみよう。すると、それは標準的なパスワードベースの認証と対象鍵暗号に基づいてファイアウォールのルールを操作する単純なシステムであることがわかる。この仮想的なデーモンを高レベル言語で実装し、他の機能を追加したいという衝動を懸命に抑え切れば、バグの大幅な減少が達成され、相当の程度まで保守不要に近いシステムを実現できる可能性があるだろう。秘匿性を追加することもできる。それにはサービスをTCPポートの代わりにUDPポート上で実行すればよい。UDPはコネクションレスのプロトコルであるため、ポートの開閉状態の概念はTCPの場合よりもずっと弱い。ポートでパケットを受信するたびにアプリケーションからICMP_PORT_UNREACHメッセージを送信する(それでいてパケットの処理は実行する)ことにより、パスワードを知らない人たちにはそのポートが閉じているように見せることができる。システム全体が十分に簡素な場合は、このような隠匿性の追加が有効と言えるかもしれない。一方、秘匿性は利用せず、代わりにチャレンジ応答プロトコルを実装することでリプレイ攻撃を確実に防ぐ方法も考えられる。

結論的には、ポートノッキングは多くのユーザが直面している問題の解決を目指す取り組みではあるものの、そのソリューションは必要以上に複雑すぎて、最善からはほど遠いと言えるだろう。