2010/11/11

CouchDBをデスクトップ用DBとして使う場合のセキュリティ上の懸念点

CouchDBはデフォルトで 127.0.0.1:5984にバインドするようになってはいますが、ローカルユーザに対するセキュリティはデフォルトでは何もありません。 loopback IPアドレスへのバインドがセキュリティだなんていわないですよね…。

デスクトップ用のDBを探しているとはいえ、Linuxは元々マルチユーザOSですし、それでなくてもいろいろ心配です。

認証(Authentication)はCouchDB全体で一つで良いですが、できれば認証はLDAPと連携したいし、承認(Authorization)はDB毎に行ないたいところです。FAQをみるとDocument毎の承認は無理みたいですね…。

たまたま CouchDBのCookie認証についての記事をみつけたので参考にしつつ、どういうシステムなのか実際に設定を変更して確認してみました。

準備作業

source codeを確認する必要がありそうなので、apt-get source couchdbと最新の apache-couchdb-1.0.1.tar.gzを展開しました。

またUbuntu上の0.10.0でも確認するようにしていますが、基本的には最新の1.0.1をmake installして確認作業をしています。

Basic認証 - 普通のWebサーバレベル

参考にさせて頂いたyssk22さんの記事だと「local.iniにユーザ名とパスワードの組を書いておく」べし、となっています。

どういう事なのかなと思ってlocal.iniファイルをみると、次のような変更が必要でした。

変更前のlocal.iniファイル抜粋

...
;WWW-Authenticate = Basic realm="administrator"
...
; require_valid_user = false
...
[admins]
;admin = mysecretpassword

変更後のlocal.iniファイル抜粋

...
WWW-Authenticate = Basic realm="administrator"
...
require_valid_user = true
...
[admins]
;admin = mysecretpassword
user1 = ab5e29d1

この設定でBasic認証が要求されるようになりましたが、少し問題もありました。

最初の問題は、local.ini に書いた生のパスワードは次のようにハッシュ化した状態で書き換えられるため、local.ini ファイルに書き込み権限がないと起動に失敗します。

user1 = -hashed-2f8dfa38b8543afaf3675d62f46c575bc96e7972,2ae6fb964a7f37818a1db077bbfc1def

これはパスワードをじかに書いた後の一度だけなので、起動してしまえば書き込み権限は不要です。

それも避けたいという場合には、直接この行を生成することもできます。

カンマで区切られた後半の文字列はSaltですから、次のようにすれば手動で生成できます。 パスワードやSaltを適宜変更することを忘れないでください。

$ echo -n "ab5e29d1""2ae6fb964a7f37818a1db077bbfc1def" | sha1sum
2f8dfa38b8543afaf3675d62f46c575bc96e7972  -

次の問題はUbuntuのパッケージから導入したり、make installすると、local.iniのファイルパーミッションが644になるところです。 CouchDB WikiのInstalling on Ubuntuでは、次のようなパーミッションを設定しています。

chown -R root:couchdb /etc/couchdb
chmod 664 /etc/couchdb/*.ini
chmod 775 /etc/couchdb/*.d

これが悪いとはいわないけれど、ガイドの記述に追加して、Otherに対するアクセス権を落しています。

$ sudo chmod 2750 /etc/couchdb
$ sudo chmod o-rwx /etc/couchdb/local.ini

Makefile.amにchmod o-rwxを入れる事もできるけれど、どういうパーミッションにするのかはサイトのポリシーの問題でもあります。 少なくともやたらと書き込み権限を落すと起動しなくなる場合があることには注意が必要でしょう。

残りはRuby用のCouchモジュールで、Basic認証の機能がないので元々準備されていた@optionsインスタンス変数を利用して、ID/Passwordを与えるように修正しました。

Couchモジュール(couchdb.rb)の変更個所

--- couchdb.rb.orig	2010-11-09 19:27:38.000000000 +0900
+++ couchdb.rb	2010-11-09 19:27:31.000000000 +0900
@@ -32,6 +32,7 @@
     end
 
     def request(req)
+      req.basic_auth @options['user'], @options['password'] if @options.kind_of?(Hash) and @options.has_key?('user') and @options.has_key?('password')
       res = Net::HTTP.start(@host, @port) { |http|http.request(req) }
       unless res.kind_of?(Net::HTTPSuccess)
         handle_error(req, res)

クライアント側コードにIDとパスワード設定を追加

--- main.rb.orig	2010-11-09 19:17:53.000000000 +0900
+++ main.rb	2010-11-09 19:18:27.000000000 +0900
@@ -9,7 +9,7 @@
 
 csv = GenCSVText.new
 
-couch = Couch::Server.new("172.16.73.197","5984")
+couch = Couch::Server.new("172.16.73.197","5984",{'user'=>'user1', 'password'=>'ab5e29d1'})
 1000.times do |doc_id|
   doc_url = "/csv/qa." + doc_id.to_s
   res = nil

Basic認証については、こんなところです。

認証方式の設定について

couchdbでの認証方法の種類を設定する方法や、その仕組みについてまとめておきます。

まずは default.ini ファイルに書かれている authentication_handlers について。

authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler},
                                {couch_httpd_auth, cookie_authentication_handler},
                                {couch_httpd_auth, default_authentication_handler}

右辺にあるタプルの前半はモジュール名で、src/couchdb/couch_httpd_auth.erl などにある関数を、2番目の *_handler で指定しています。

default_authentication_handler が実際に使われる主な処理をしているところだと思います。

使える候補としては、couch_httpd_auth.erlで-export()で宣言されている関数は次の通りです。

-export([default_authentication_handler/1,special_test_authentication_handler/1]).
-export([cookie_authentication_handler/1]).
-export([null_authentication_handler/1]).
-export([proxy_authentification_handler/1]).
-export([cookie_auth_header/2]).

yssk22さんの説明は助かっていますが、残念ながらCookie認証についてはほとんど理解できていません。 Cookie認証は少し放っておいて、コメントのある proxy_authentification_handler を試すことにしました。

proxy_authentification_handler について

Ubuntu 10.04 LTSのパッケージで導入している0.10.0では、 proxy_authentification_handlerはありません。 CHANGESファイルによれば、0.11.0から導入されたようです。 ここから先は手動でVMWare上のUbuntu 10.04に導入した、1.0.1で試しています。

この proxy_authentification_handler はコメントを読むと、認証を他のシステムで行なった結果を送ってもらい、couchdbとその認証システムで共有しているtokenをキーに生成した情報を照合するという仕組みのようです。

Cookieのような時間制限をどうやって行なっているんだろうと思いつつ、試してみることにします。

proxy_authentification_handler の設定

クライアントはどうにかして、HTTPヘッダにいくつかのパラメータを設定します。

  • X-Auth-CouchDB-UserName - 'admin'や'user1'といったBasic認証と同じようなID名
  • X-Auth-CouchDB-Roles - Basic認証でいうところの'admin'です
  • X-Auth-CouchDB-Token - secret と X-Auth-CouchDB-UserNameを連結した文字列のSHA1 Message Digest

対応するlocal.iniの[couch_httpd_auth]に設定するパラメータは次のとおりです。

  • (必須)proxy_use_secret - "true"の時にX-Auth-CouchDB-Tokenの照合を行ない、それ以外の値であれば無条件にX-Auth-CouchDB-UserNameのUser名とX-Auth-CouchDB-Rolesのロールを設定
  • (必須)secret - X-Auth-CouchDB-Tokenを生成に使う文字列
  • x_auth_username - X-Auth-CouchDB-UserNameの別名を定義
  • x_auth_roles - X-Auth-CouchDB-Rolesの別名を定義
  • x_auth_token - X-Auth-CouchDB-Tokenの別名を定義
サーバ側の設定作業

まずは default.ini に、proxy_authentification_handler を設定します。

default.iniファイルの該当行の抜粋

authentication_handlers = {couch_httpd_auth, proxy_authentification_handler}

次に local.ini に、必須な2つのフィールドを追加します。

proxy_authだけを有効にする場合のcouch_httpd_authセクション

[couch_httpd_auth]
secret = 329435e5e66be809a656af105f42401e
proxy_use_secret = true

これで一応はサーバ側の準備はできましたが、問題はクライアントの準備です。

クライアント側の設定作業

さきほどのcouchdb.rbにさらに変更を加えることにしました。

couchdb.rb変更部分のdiff

--- couchdb.rb.orig	2010-11-10 20:44:14.000000000 +0900
+++ couchdb.rb	2010-11-10 20:45:04.000000000 +0900
@@ -33,6 +33,9 @@
 
     def request(req)
       req.basic_auth @options['user'], @options['password'] if @options.kind_of?(Hash) and @options.has_key?('user') and @options.has_key?('password')
+      req["X-Auth-CouchDB-UserName"] = @options['proxy_auth_user'] if @options.kind_of?(Hash) and @options.has_key?('proxy_auth_user')
+      req["X-Auth-CouchDB-Roles"] = @options['proxy_auth_roles'] if @options.kind_of?(Hash) and @options.has_key?('proxy_auth_roles')
+      req["X-Auth-CouchDB-Token"] = @options['proxy_auth_token'] if @options.kind_of?(Hash) and @options.has_key?('proxy_auth_token')
       res = Net::HTTP.start(@host, @port) { |http|http.request(req) }
       unless res.kind_of?(Net::HTTPSuccess)
         handle_error(req, res)

main.rb変更部分のdiff

--- main.rb.orig	2010-11-10 20:43:54.000000000 +0900
+++ main.rb	2010-11-10 20:27:55.000000000 +0900
@@ -8,7 +8,9 @@
 require 'json'
 
 csv = GenCSVText.new
-couch = Couch::Server.new("127.0.0.1","5984",{'user'=>'user1', 'password'=>'ab5e29d1'})
+couch = Couch::Server.new("127.0.0.1","5984",{'user'=>'user1', 'password'=>'ab5e29d1',
+		:proxy_auth_user => "user1", :proxy_auth_roles => "_admin,_users",
+		:proxy_auth_token => "d4c3b0fd10bed9642fb5bbfcc0203ca27c707300" })
 10.times do |doc_id|
   doc_url = "/csv/qa." + doc_id.to_s
   res = nil

実際に使う段階になると、 proxy_auth_token の生成方法をどうしようかなと思いました。

erlを使って文字列を生成する方法は次のようになります。

couch_util.erl か couch_util.beam のあるディレクトリ(今回は ~/apache-couchdb-1.0.1/src/couchdb/)をpathに追加するため、 -paオプションに渡しています。 ファイルが存在すれば、/usr/lib/couchdb/erlang/lib/couch-0.10.0/ebin などでも構いません。

$ erl -pa ~/apache-couchdb-1.0.1/src/couchdb/
1> nl(couch_util).
2> nl(crypto).
3> crypto:start().
4> D = <<"user1">>.
5> KK = <<"329435e5e66be809a656af105f42401e">>.
6> couch_util:to_hex(crypto:sha_mac(KK,D)).

最後のコマンドを入力すると、この場合は "d4c3b0fd10bed9642fb5bbfcc0203ca27c707300" になります。

改造したcouchdb.rbを利用するmain.rbの全体は、次のようになっています。

CouchDB独自のProxy認証を行なうmain.rb全体

#!/usr/local/bin/ruby1.9 -I.
# -*- encoding: UTF-8 -*-

$:.unshift File.dirname($0)

require 'couchdb'
require 'json'

couch = Couch::Server.new("127.0.0.1","5984",{:proxy_auth_user => "user1", 
                :proxy_auth_roles => "_admin",
		:proxy_auth_token => "d4c3b0fd10bed9642fb5bbfcc0203ca27c707300" })

res = couch.get("/_all_dbs")
p JSON.parse(res.body)

2010/11/17追記:
このスクリプトとcouchdb.rbは同じディレクトリにあると仮定しています。
couchdb.rbがカレントディレクトリにない場所からパスを指定してスクリプトを起動した場合に、動かないため$:.unshift "."だった記述を修正しました。

パスワードに相当する情報はtokenに何も反映されませんから、MACの強度にだけ依存しています。 せめてMACに与えるSecretにtoken以外のユーザ毎に変化する何かも加えられれば良かったんですけどね。

全体で一つのtokenに依存していますから、頻繁にtokenを入れ替えない限りはちょっと使いたくない感じのものだという事はわかりました。

ちなみに、WebブラウザのProxyに指定してX-CouchDB-Auth-*ヘッダを追加するProxyサーバのコードも載せておきます。

手元のテンプレートを元に作成したProxyサーバ

#!/usr/bin/ruby

require "socket"
require "uri"

gs = TCPServer.open("localhost", 8880)
while TRUE
  Thread.start(gs.accept) do |s|
    ## prepare and modify request header part
    remote = nil
    begin
      h = Array.new
      while (l = s.gets)
        break if l =~ /^\r\n|^\n/
        if l =~ /^Keep-Alive:/
          ## ignore the keep-alive header
        elsif l =~ /Proxy-Connection:/
          ## remove the proxy-connection header because of no keep-alive support.
          h << "Connection: close\r\n"
        elsif l =~ /Auth/
          p l
        elsif l =~ /^Cookie/
          p l
        else
          h << l
        end
      end
      h << "X-Auth-CouchDB-UserName: user1\r\n"
      h << "X-Auth-CouchDB-Roles: _admin\r\n"
      h << "X-Auth-CouchDB-Token: d4c3b0fd10bed9642fb5bbfcc0203ca27c707300\r\n"
      h << "\r\n"

      ## override first line of header
      hp, huri, hv = h[0].split(/\s+/)
      raise "wrong request header" if hp.nil? or hv.nil?
      uri = URI.parse(huri)
      p huri
      h[0] = "#{hp} #{uri.path} #{hv}\r\n"

      ## open endpoint
      remote = TCPSocket.open(uri.host, uri.port)
      ## send header info
      h.each {|i|
        remote.puts i
      }
      
      ## get response
      while(l = remote.gets)
        s.puts l
        break if l =~ /^\r\n|^\n/
      end

      ## get body
      while (l = remote.gets)
        s.puts l
      end
    ensure
      remote.close
      s.close
    end
  end
end

このスクリプトを走らせた後に、WebブラウザのProxy設定で ホスト名"127.0.0.1", ポート番号"8880"を指定してアクセスすればCouchDBのWebフロントエンド Futon にアクセスできます。

このProxyを経由して他のサーバにアクセスに行くと、余計な認証情報をばらまく事になるので、URIの解析を始めに行なってh << "X-Auth-CouchDB-UserName: user1\r\n" if uri.port == "5984"ぐらいしておくと安心かもしれません。

proxy_authentification_handler を使った感想

X-Auth-CouchDB-*のヘッダを付与しないリクエストを送信する一般ユーザでもDBの参照は可能なままです。

これは handler のコードが次のようになっていて、実際の処理を行なうproxy_auth_user関数がnilを返した時には拒否をせずに現状のReqコンテキストを返すことに起因しているようです。

couch_httpd_auth.erlのhandler関数全体

proxy_authentification_handler(Req) ->
    case proxy_auth_user(Req) of
        nil -> Req;
        Req2 -> Req2
    end.

Proxyを経由しないアクセスを拒否するのであれば、Reqを返さずにnil -> throw({unauthorized, <<"token is incorrect.">>});;ぐらいにすると、{"error":"unauthorized","reason":"token is incorrect."} という返答が返るようになります。

cookie_authentication_handlerは良さそうだけれど、とりあえずは default_authentication_handler を設定して、proxy_authentification_handler が失敗した場合でもBasic認証がかかるようにしておきました。

次は cookie_authentication_handler について、 Secure Cookie Authentication for CouchDBをまずは読んでみようとしています。

ファイルレベルのパーミッションについて

ファイルのパーミッションは、個々の作業の中でもいろいろ出てきたので、まとめておきたいと思います。

とりあえずUbuntuで標準的に導入されるパッケージの設定では/var/lib/couchdbのディレクトリレベルでパーミッションを0750になっています。

認証を行なう場合は default.ini や local.ini あるいは、DB自体に何らかの情報を含む可能性があるので、それらのディレクトリの所有者/グループを root:couchdb に変更し、サーバが書き込む必要のないところは、g-wX,o-rwX、必要なところは g+wX,o-rwX ぐらいをchmodで設定するのがベストです。

ただ、それでも新規に作成されるDBなどのファイルは644で、バックアップファイルの扱いなど懸念があります。 起動スクリプトに'.'で読み込まれる /etc/default/couchdb にumask 027の一行を加えておきました。

Ubuntu 10.04 LTSで使っているCouchDB用には、次のような設定をしています。

$ sudo chmod -R o-rwX /var/lib/couchdb
$ sudo chmod g+s /var/lib/couchdb /var/lib/couchdb/0.10.0
$ sudo chown -R root:couchdb /etc/couchdb
$ sudo chmod -R g-w,g+rX,o-rwX /etc/couchdb
$ sudo chmod g+s /etc/couchdb
$ echo umask 027 | sudo tee -a /etc/default/couchdb

default.ini, local.ini をcouchdbプロセスが書き込めるようにするかは、必要性とポリシー次第かなと思います。

Linuxなら /etc/login.defs 辺りで、UMASK 027 を設定するべきかなとも思います。

まとめ

微妙に綴りの違う、*_authentication_handler と proxy_authenti fication_handler がまぎらわしい。

CouchDBは良さそうだけど、エラーメッセージ系はもうちょっと改善してほしいところ。

冗談ではないけれど、もう少しまじめに書くと、cookie認証にあるようなtimestampの概念はProxy認証では導入されていませんでした。 これは必要じゃないかなと感じてcookie認証についてのドキュメントとコードを読み始めています。

Proxy認証は自分で認証モジュールを作りたい場合の雛型コードとしてはシンプルで良いけれど、実用的なものではなさそうだというのが今のところの印象です。

経緯を調べてみたんですが、みつけられなかったんですよね。

0 件のコメント: