2010/11/26

CouchDB: ApacheをReverse Proxyサーバにしてみた

認証とSSL化はApacheをフロントエンドにするのが良いよね、と考える人は大勢いるようで Wikiにドキュメントの「Apache Reverse Proxy for same origin and authentication」で解説されています。

この手順の問題点は null_authentication_handler を使うように指示しているところで、 Security Features Overviewに記述されているSecurity Modelを無視する形になるところです。

そこで認証自体はApacheで行なって、ヘッダについている"Authorization"行を解析してユーザ名を取り出して、/_users/org.couchdb.user:$usernameを参照してRolesを設定するようにCouchDBを改造してみました。

注意事項

今回作成したhandlerは渡された情報を信用し、自分でパスワードの妥当性を検証しません。

ApacheではBasic認証とDigest認証が選択できますが、Basic認証に対応する"Authorization Basic ..."ヘッダは"default_authentication_handler"につかまってしまうため、"default_authentication_handler"を有効にすることができません。

"default_authentication_handler"は不要じゃないか、と思うかもしれませんが、127.0.0.1:5984から接続する際に問題が発生します。

"Authorization Basic ..."ヘッダを捏造してアクセスすると、"webproxy_authentication_handler"はパスワードの妥当性を検証しないため、任意のユーザになりすます事ができてしまいます。

今回のようにApacheでDigest認証を有効にして、"require_valid_user = true"にした設定で、"default_authentication_handler"を併用するのがお勧めです。

想定される解決策

adminsや/_users以下の文書にパスワードが記載されているユーザについては、Basic認証の時にパスワードが一致するか検証する事ができます。

この変更は少し時間をみてやってみようと思います。

Proxy認証のようにsaltを元にした情報をくっつける方法は、今回のようにlocalhostにログインできるユーザへの対応としては適当ではないと考えています。

使い方

couch_httpd_auth.erlに対するpatchは後半に載せています。 適当なファイル("apache-couchdb-1.0.1.webproxy.diff")にして、patchコマンドで適用していきます。

$ ls -ld apache-couchdb-1.0.1
<samp>drwxr-sr-x 11 user1 user1 4096 2010-08-12 03:19 apache-couchdb-1.0.1</samp>
$ patch -p0 < apache-couchdb-1.0.1.webproxy.diff
patching file apache-couchdb-1.0.1/src/couchdb/couch_httpd_auth.erl

あとはsrc/couchdbディレクトリに移動してmakeするなり、erlc couch_httpd_auth.erlでbeamファイルに変換してから、既存のファイルと置き換えて全体をリスタートします。

$ cd apache-couchdb-1.0.1/src/couchdb/
$ make
$ sudo cp couch_httpd_auth.beam /usr/local/lib/couchdb/erlang/lib/couch-1.0.1/ebin/
$ sudo /etc/init.d/couchdb restart

/etc/init.d/couchdbファイルは/usr/local/etc/init.d/couchdbファイルをコピーしたものです。

local.iniファイルの編集

関連する設定項目は次のとおりですが、default.iniとの差分を全部載せるのは大変です。 CouchDBの設定については/_configを経由して、今回の場合は/_config/httpdと/_config/couch_httpd_authの出力が役に立つはずです。

local.ini

[httpd]
WWW-Authenticate = Basic realm="administrator"
authentication_handlers = {couch_httpd_auth, default_authentication_handler}, {couch_httpd_auth, webproxy_authentication_handler}

[couch_httpd_auth]
require_valid_user = true
require_authentication_db_entry = true

local.iniファイルに記述したadminsのID(admin), Password(password)を使って設定内容を出力してみます。

$ curl -u admin:password http://127.0.0.1:5984/_config/httpd
{
  "max_connections":"2048",
  "bind_address":"127.0.0.1",
  "vhost_global_handlers":"_utils, _uuids, _session, _oauth, _users",
  "port":"5984",
  "default_handler":"{couch_httpd_db, handle_request}",
  "secure_rewrites":"true",
  "allow_jsonp":"false",
  "authentication_handlers":"{couch_httpd_auth, default_authentication_handler}, {couch_httpd_auth, webproxy_authentication_handler}",
  "WWW-Authenticate":"Basic realm=\"administrator\""
}
$ curl -u admin:password http://127.0.0.1:5984/_config/couch_httpd_auth
{
  "auth_cache_size":"50",
  "timeout":"600",
  "secret":"329435e5e66be8a9a652af105f42401e",
  "require_valid_user":"true",
  "require_authentication_db_entry":"true",
  "authentication_db":"_users",
  "authentication_redirect":"/_utils/session.html"
}
Apache側の設定

Ubuntu Server 10.04 LTS付属のApacheを使っています。 SSLを有効にするような細かい設定は省いて、ポイントになるところだけ抜粋しておきます。

port80(http)のRedirect設定

<VirtualHost couch.example.org:80>
...
<IfModule mod_alias.c>
        Redirect permanent / https://couch.yasundial.org/
</IfModule>
</VirtualHost>

port443(https)のCouchDBへのProxy設定

<IfModule mod_ssl.c>
<VirtualHost couch.example.org:443>
...
        SSLCertificateFile    /etc/ssl/certs/ssl-cert-couch.pem
        SSLCertificateKeyFile /etc/ssl/private/ssl-key-couch.pem
...
    <Location />
## Digest Auth
           AuthType Digest
           AuthName "CouchDB"
           AuthDigestDomain /
           AuthDigestProvider file
           AuthUserFile /etc/apache2/htdigest.db
## end of Digest Auth
## Basic Auth
#           AuthType Basic
#           AuthName "CouchDB"
#           AuthUserFile /etc/apache2/htpassword.db
## end of Basic Auth
           Require valid-user
     </Location>

<IfModule mod_proxy.c>
        ProxyPass / http://127.0.0.1:5984/
        ProxyPassReverse / http://127.0.0.1:5984/
</IfModule>
</VirtualHost>
</IfModule>

設定ファイルにあって、新しく配置したファイルと作成方法の概略は次の通りです。

/etc/ssl/certs/ssl-cert-couch.pem, /etc/ssl/private/ssl-key-couch.pem
$ /usr/lib/ssl/misc/CA.pl -newreq
$ /usr/lib/ssl/misc/CA.pl -sign
$ rm newreq.pem
$ cp newcert.pem ssl-cert-couch.pem
$ cp newkey.pem ssl-key-couch.pem
/etc/apache2/htdigest.db
$ sudo htdigest -c htdigest.db CouchDB admin
/etc/apache2/htpassword.db
$ sudo htpasswd -c htpassword.db admin

変更を加えたcouch_httpd_auth.erl用のパッチ全体

apache-couchdb-1.0.1.webproxy.diffファイル全体

--- apache-couchdb-1.0.1/src/couchdb/couch_httpd_auth.erl	2010-06-23 22:21:30.000000000 -0700
+++ apache-couchdb-1.0.1.webproxy_auth/src/couchdb/couch_httpd_auth.erl	2010-11-25 19:09:49.000000000 -0800
@@ -17,6 +17,7 @@
 -export([cookie_authentication_handler/1]).
 -export([null_authentication_handler/1]).
 -export([proxy_authentification_handler/1]).
+-export([webproxy_authentication_handler/1]).
 -export([cookie_auth_header/2]).
 -export([handle_session_req/1]).
 
@@ -347,3 +348,102 @@
 make_cookie_time() ->
     {NowMS, NowS, _} = erlang:now(),
     NowMS * 1000000 + NowS.
+
+%%
+%% webproxy auth handler %%
+%%
+%% This handler allows a user authentication by an external system.
+%% It expects the external system passes 'Authorization Basic' or 'Authorization Digest' header.
+%% The authenticated username and corresponding user roles will be set into the userCtx object.
+%% Corresponding user roles are referred from the /$authentication_db/org.couchdb.user:$username document.
+%%
+%% The following article suggested to use the null_authentication_handler, but it doesn't maintain userCtx object.
+%% ->  http://wiki.apache.org/couchdb/Apache_As_a_Reverse_Proxy 
+%%
+%% This handler implicitly uses new config entry, require_authentication_db_entry, the possible value is true or false.
+%%   If it's true, then the $username document is required to set username and roles to the userCtx object.
+%%   Otherwise, the authentication will be failed.
+%%   It's the default behavior.
+%%
+%%   If it's false and there is no corresponding $username document at $authentication_db, 
+%%   then the only $username will be set into the userCxt object with empty roles.
+%%
+%% Note: The password has never been used, but only username is referred.
+%% Note: The digest authentication is recommended because default_authentication_handler will trap the basic authentication header.
+%% 
+webproxy_authentication_handler(Req) ->
+    AuthorizationHeader = header_value(Req, "Authorization"),
+    case AuthorizationHeader of
+	"Basic " ++ _ -> 
+	    webproxy_basic_auth(Req);
+	"Digest " ++ DigestValue ->
+	    webproxy_digest_auth(Req, DigestValue);
+	_ -> webproxy_default_terminate_action(Req)
+    end.
+
+webproxy_digest_find_user([H|T]) ->
+    case H of
+	["username",U] -> 
+	    %% RFC2069 says U must be a quoted-string, so remove double quote charaters.
+	    User = string:sub_string(U,2,string:len(U)-1),
+	    ["username",User];
+	_ -> webproxy_digest_find_user(T)
+    end.
+
+webproxy_default_terminate_action(Req) ->
+    %% reference: http://wiki.apache.org/couchdb/Security_Features_Overview
+    case couch_server:has_admins() of
+        true ->
+            Req;
+        false ->
+            case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
+                "true" -> Req;
+		%% If no admins, and no user required, then everyone is admin!
+		%% Yay, admin party!
+                _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
+            end
+    end.
+
+webproxy_basic_auth(Req) ->
+    case basic_name_pw(Req) of
+	{User, _} ->
+	    case couch_auth_cache:get_user_creds(User) of
+		nil ->
+		    case couch_config:get("couch_httpd_auth", "require_authentication_db_entry", "true") of
+			"true" -> 
+			    throw({unauthorized, <<"Name couldn't be found on authentication_db.">>});
+			_ ->
+			    Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[]}}
+		    end;
+		UserProps ->
+		    Req#httpd{user_ctx=#user_ctx{
+				name=?l2b(User),
+				roles=couch_util:get_value(<<"roles">>, UserProps, [])
+			       }}
+	    end;
+	_ -> webproxy_default_terminate_action(Req)
+    end.
+
+webproxy_digest_auth(Req, DigestValue) ->
+    %% DigestValue might be "username=\"yasu\", realm=\"CouchDB\", ..."
+    DigestKVSplitFun = fun(X) -> string:tokens(string:strip(X), "=") end, 
+    DigestItemList = [DigestKVSplitFun(X) || X <- string:tokens(DigestValue,",")],
+    %% DigestItemList might be [[key0,value0], ["username","yasu"], [key1,value1], ...]
+    case webproxy_digest_find_user(DigestItemList) of
+	["username", User] -> 
+            case couch_auth_cache:get_user_creds(User) of
+		nil ->
+		    case couch_config:get("couch_httpd_auth", "require_authentication_db_entry", "true") of
+			"true" -> 
+			    throw({unauthorized, <<"Name couldn't be found on authentication_db.">>});
+			_ ->
+			    Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[]}}
+		    end;
+		UserProps -> 
+		    Req#httpd{user_ctx=#user_ctx{
+				name=?l2b(User),
+				roles=couch_util:get_value(<<"roles">>, UserProps, [])
+			       }}
+            end;
+	_ -> webproxy_default_terminate_action(Req)
+    end.

0 件のコメント: