前回投稿した記事でApacheをReverse Proxyサーバにするための認証ハンドラについて書きました。
セキュリティ上の問題はいくつかあって解決策を考えたのですが、最大の問題はCouchDBの待ち受けポートに直接接続した場合にAuthorization Headerを詐称された場合の対応です。 次善の策としてApacheとCouchDBだけが知るキーワードをHTTP Request Headerに加える事にしました。
究極的にはWASのIHS pluginのように、フロントエンドとバックエンドの経路を暗号化するようなapache用のプラグインを開発するのがベストな方法だとは思います。 もちろん、コストに合わないので、そんな面倒なことはやめました。
加えた変更の概要
CouchDBに設定する secretをキーと値にしてHMACSHA1を計算させています。 これは単純にsecretを使いたくないというだけで、強度についてはあまり問題にしていません。
双方で計算可能な特別な値を X-Auth-CouchDB-Tokenヘッダの値として渡しています。 具体的には次のような設定をApacheにしています。
追加したProxyPass設定前後の抜粋
...
<IfModule mod_proxy.c>
<IfModule mod_headers.c>
RequestHeader add X-Auth-CouchDB-Token "c21ec459f6a650dcf6907f2b52e611a069a7aeee"
</IfModule>
ProxyPass / http://127.0.0.1:5984/
ProxyPassReverse / http://127.0.0.1:5984/
</IfModule>
...
対応するCouchDBのlocal.iniの内容は次のようになっています。
local.iniファイルの内容抜粋
[httpd]
WWW-Authenticate = Basic realm="administrator"
authentication_handlers = {couch_httpd_auth, webproxy_authentication_handler}
[couch_httpd_auth]
require_valid_user = true
;require_authentication_db_entry = false
webproxy_use_secret = true
secret = 329435e5e66be809a656af105f42401e
独自に追加したiniエントリの説明
加えたrequire_authentication_db_entryのデフォルト値は"true"、webproxy_use_secretのデフォルト値は"false"です。
require_authentication_db_entryについて
このデフォルト値ではauthenticate_db(e.x. '_users')にユーザ名('username')に対応する文書(e.x. '/_users/org.couchdb.user:username')が存在しないと認証に失敗します。
require_authentication_db_entryを"false"にすると対応する文書がなくてもアクセス可能で、その場合は文書があればroleが設定され、なければroleは空"[]"になります。
webproxy_use_secretについて
この値をtrueにして始めて、Apache側でX-Auth-CouchDB-Tokenを付けてProxyPassするように設定を変更します。
SHA1のHMACを計算するためには、 以前に投稿したのと同じ方法で行ないます。
$ erl -pa /usr/local/lib/couchdb/erlang/lib/couch-1.0.1/ebin 1> nl(couch_util). 2> nl(crypto). 3> crypto:start(). 4> Secret = <<"329435e5e66be809a656af105f42401e">>. 5> couch_util:to_hex(crypto:sha_mac(Secret,Secret)). "c21ec459f6a650dcf6907f2b52e611a069a7aeee"
最後に表示された値をX-Auth-CouchDB-Tokenに指定します。
これらの値を知っている管理者はcurl等を使って、アクセスすることもできます。
$ curl -H 'X-Auth-CouchDB-Token: c21ec459f6a650dcf6907f2b52e611a069a7aeee' -u admin: http://127.0.0.1:5984/_session
パスワードが空になっているところがポイントで、危険なところです。
まとめ
今回はCouchDBとApacheが同じサーバで動くので、過剰な対応に思える部分もあったかもしれません。
Apacheをフロントエンドとして別のサーバにたてる場合には、今回の対応では不十分で、バックエンドサーバ(CouchDB)のポートに対する接続元の制御などの配慮が必須になります。
少なくともnull_authentication_handlerよりもましですが、proxy_authentification_handlerと同等かさらに悪い実装になっています。
Apache側で動的に変化する値をヘッダに付与できると良かったと思います。 それができればCookie認証のように時間情報を加えてHMACを生成してヘッダに加えることができたのですが、今回はできませんでした。
もう少し使ってみて、不具合がないか確認することにします。
パッチ其の弐
gzで圧縮したファイル、 20101126.2.couchdb101.webproxy.diff.gzもあります。
--- apache-couchdb-1.0.1.orig/src/couchdb/couch_httpd_auth.erl 2010-06-23 22:21:30.000000000 -0700
+++ apache-couchdb-1.0.1/src/couchdb/couch_httpd_auth.erl 2010-11-26 06:23:51.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,127 @@
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 will be 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 uses new config entry, require_authentication_db_entry, the possible value is true or false.
+%% If it's true, then the authentication_db document should be existing for each authenticated user.
+%% It's the default behavior.
+%%
+%% If it's false and there is no corresponding $username document at $authentication_db,
+%% then the $username and empty role will be set into the userCxt object.
+%%
+%% Security considerations:
+%% If an user connects to couchdb's port directly, such as curl http://127.0.0.1:5984/,
+%% with a dummy header, like 'Authorization: Digest username="admin"', then the user will get the admin user's priviledge.
+%%
+%% There is another config entry, webproxy_use_secret, as an option.
+%% If it's true, then the X-Auth-CouchDB-Token request header is expected, borrowed from proxy_authentication_handler.
+%% The value is a static and should be same as the result of couch_util:to_hex(crypto:sha_mac(Secret, Secret)).
+%% The Secret is the secret key in couch_httpd_auth section of ini.
+%%
+webproxy_authentication_handler(Req) ->
+ XHeaderToken = couch_config:get("couch_httpd_auth", "x_auth_token", "X-Auth-CouchDB-Token"),
+ case couch_config:get("couch_httpd_auth", "webproxy_use_secret", "false") of
+ "true" ->
+ case couch_config:get("couch_httpd_auth", "secret", nil) of
+ nil ->
+ throw({unauthorized, <<"scret should be defined on couch_httpd_auth.">>});
+ Secret ->
+ ExpectedToken = couch_util:to_hex(crypto:sha_mac(Secret, Secret)),
+ case header_value(Req, XHeaderToken) of
+ Token when Token == ExpectedToken ->
+ webproxy_authentication_handler_main(Req);
+ _ ->
+ throw({unauthorized, <<"unmatch token header.">>})
+ end
+ end;
+ _ -> webproxy_authentication_handler_main(Req)
+ end.
+
+webproxy_authentication_handler_main(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 件のコメント:
コメントを投稿