2010/11/30

CouchDB: Ruby CouchモジュールをDigest認証対応にする

CouchDBのGetting startedには Ruby用のCouchモジュールが掲載されています。

Basic認証に対応させたり、SSLクライアント認証対応にしたりしてきましたが、今回はDigest認証に対応させる事にしました。

これはCouchDBの標準機能ではありません。フロントエンドにApacheなどでProxy Serverを配置し、そのProxy ServerがDigest認証を行なう事を想定しています。

なおDigest認証の仕様は RFC2069 - An Extension to HTTP : Digest Access Authenticationで定義されています。

使い方のサンプル

Couch::Server#newメソッドの第三引数のハッシュに'digest_auth'プロパティを設定します。 他はBasic認証と同じです。

サンプルコード

couch =  Couch::Server.new("couch.example.org","443", {
                           'digest_auth' => 'true',
		           'user' => 'admin',
		           'password' => 'xxxxxxxxxxxx',
		         ...})

他は同じように使えますが、ループの中で繰り返し実行するような場合には#get, #post, #put, #deleteなどの各メソッドで再認証を求められる可能性を考慮する必要があります。

open(file).each_line do |line|
  row = 
  json = Hash.new
  ...
  res = ""
  while not res.kind_of?(Net::HTTPSuccess)
    begin
      res = couch.post(uri, json.to_json)
    rescue
      p $!
    end
  end
end

ライブラリを少し変更すると自動的に再認証することもできますが、最大回数を設定するなどの配慮が必要になるでしょう。

Digest情報のキャッシュ

今回利用したライブラリのサンプルでは再利用の方法について、サンプルはないようでした。

RFC2069では、再利用できる情報としてusername, password, nonce, nonce count and opaque valuesが挙げられています。

これらの情報を使いまわすようにしていますが、手元のApacheを使った環境では5分毎に再認証(rc=401)を求められます。

この挙動への対応として、ライブラリ側でのコントロールをしない方法を選択しました。 オリジナルのライブラリで行なっていたレスポンスコードに応じた例外の送出は行なわれません。

レスポンスオブジェクトはそのままクライアントに返すので、クライアント側では再認証が発生した場合に再度メソッドを呼び出すなど判断をしてください。

net-http-digest_authモジュールの導入

標準ライブラリではDigest認証に対応しません。今回は net-http-digest_authを利用しました。

このライブラリのロードはdigest_authプロパティを設定した場合に発生するので、クライアント側でrubygemsを呼び出すなどの準備が必要です。

couchdb.rbと同じディレクトリにnetディレクトリを配置した場合の設定例

main.rbからcouchdb.rbを呼び出す、次のようなディレクトリ構造を想定しています。

bin/main.rb
lib/
lib/couchdb.rb
lib/net/http/digest_auth.rb

main.rbでは次のようにライブラリをロードします。

netディレクトリを手動で配置した場合の設定例

$:.unshift File.join([File.dirname($0),"..","lib"])
require 'couchdb'
gemsを使う場合の設定例

今回もmain.rbからcouchdb.rbを呼び出す、次のようなディレクトリ構造を想定しています。

$ cd lib
$ gem install -i gems net-http-digest_auth
bin/main.rb
lib/couchdb.rb
lib/gems/gems/net-http-digest_auth-1.0/lib/net/http/digest_auth.rb

標準以外の場所にgemsディレクトリがある場合の対応は以下のようになります。 (-iオプションを使わずに)標準的な場所にインストールした場合には、当然、ENV['GEM_HOME']の行は不要です。

gemsを使う場合の設定例

ENV['GEM_HOME'] = File.join([File.dirname($0),"..","lib","gems"])
require 'rubygems'
$:.unshift File.join([File.dirname($0),"..","lib"])
require 'couchdb'

変更を加えたCouchモジュール

モジュールの全体は次の通りです。

変更したCouchモジュール: lib/couchdb.rb

# -*- coding: utf-8 -*-

require 'net/https'

#
# This module comes from the couchdb wiki;
# http://wiki.apache.org/couchdb/Getting_started_with_Ruby
#
# Modifyed by Yasuhiro ABE - yasu@yasundial.org
#
module Couch

  class Server
    def initialize(host, port, options = nil)
      @host = host
      @port = port
      @options = options
      @options = Hash.new if options.nil? or not options.kind_of?(Hash)
      @www_auth = nil
      @auth = nil
      if options.has_key?('digest_auth')
        require 'net/http/digest_auth'
        @digest_auth = Net::HTTP::DigestAuth.new
      end
    end

    def delete(uri)
      setup_digest_auth(uri,'DELETE')
      request(Net::HTTP::Delete.new(uri))
    end

    def get(uri)
      setup_digest_auth(uri,'GET')
      request(Net::HTTP::Get.new(uri))
    end

    def put(uri, json)
      setup_digest_auth(uri,'PUT')
      req = Net::HTTP::Put.new(uri)
      req["content-type"] = "application/json"
      req.body = json
      request(req)
    end

    def post(uri, json)
      setup_digest_auth(uri,'POST')
      req = Net::HTTP::Post.new(uri)
      req["content-type"] = "application/json"
      req.body = json
      request(req)
    end

    def check_ssl(client)
      if @options.has_key?('cacert')
        client.use_ssl = true
        client.ca_file = @options['cacert']
        client.verify_mode  = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
        client.verify_mode  = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
        client.verify_depth = @options['ssl_verify_depth'] if @options.has_key?('ssl_verify_depth')
        client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
        client.cert         = @options['ssl_client_cert'] if @options.has_key?('ssl_client_cert')
        client.key          = @options['ssl_client_key'] if @options.has_key?('ssl_client_key')
      end
    end

    def request(req)
      req.basic_auth @options['user'], @options['password'] if @options.has_key?('user') and @options.has_key?('password') and not @options.has_key?('digest_auth')
      req["X-Auth-CouchDB-UserName"] = @options['proxy_auth_user'] if @options.has_key?('proxy_auth_user')
      req["X-Auth-CouchDB-Roles"] = @options['proxy_auth_roles'] if @options.has_key?('proxy_auth_roles')
      req["X-Auth-CouchDB-Token"] = @options['proxy_auth_token'] if @options.has_key?('proxy_auth_token')
      
      client = Net::HTTP.new(@host, @port)
      check_ssl(client)
      
      if @options.has_key?('digest_auth')
        req["Authorization"] = @auth
      end
      
      res = client.start { |http| http.request(req) }
      @www_auth = nil if res.kind_of?(Net::HTTPUnauthorized) and @options.has_key?('digest_auth')
      res
    end
    
    private

    def setup_digest_auth(uri, method)
      return if not @options.has_key?('digest_auth')
      if @www_auth == nil
        req = Net::HTTP::Get.new(uri)
        client = Net::HTTP.new(@host, @port)
        check_ssl(client)
        res = client.start { |http| http.request(req) }
        ## res must be the Net::HTTPUnauthorized
        raise res if not res.kind_of?(Net::HTTPUnauthorized)
        @www_auth = res['www-authenticate']
      end
      url = TinyURI.new(@options['user'], @options['password'], uri)
      @auth = @digest_auth.auth_header(url, @www_auth, method)
    end
  end

  private
  # net/http/digest_auth using this class to pass information.
  class TinyURI  # :nodoc:all
    attr_accessor :request_uri, :user, :password
    def initialize(user, pass, path)
      @user = user 
      @password = pass
      @request_uri = path
    end
  end
end

2010/11/30 22:00追記
認証に失敗した場合に@www_authを初期化するコードが抜けていたので追記しています。

モジュールの使い方: README.rd

手元で書いたREADME.rbの内容をそのまま転載します。

ruby用Couchモジュールについて

http://wiki.apache.org/couchdb/Getting_started_with_Ruby に掲載されているモジュールをベースにしています。

追加した機能は次の通りです。

  • Proxy認証 (couch_httpd_auth:proxy_authentification_handler用)
  • SSLクライアント認証 (stunnelを想定したセキュア接続用)
  • Basic認証 (couch_httpd_auth:default_authentication_handler, apache等proxy serverでの認証用)
  • Digest認証 (apache等proxy serverでの認証用)

基本的な使い方はCouch::Server.new(host,port,options)でインスタンス生成時のoptions引数にハッシュを与えます。

プロパティ認証用プロパティ

optionsにプロパティが設定されていない場合には何もしません。 値がセットされている場合に、各ヘッダにその値を指定します。

  • proxy_auth_user (X-Auth-CouchDB-UserNameヘッダの値にセット)
  • proxy_auth_roles (X-Auth-CouchDB-Rolesヘッダの値にセット)
  • proxy_auth_token (X-Auth-CouchDB-Tokenヘッダの値にセット)

現状ではこれらの値の設定をサポートするメソッドは提供されません。

Basic and Digest認証用プロパティ

認証を有効にするためには、user, password両方とも設定する必要があります。

  • user (任意の文字列)
  • password (任意の文字列)
Digest認証用プロパティ

次の"digest_auth"が設定され、@options.has_key?('digest_auth')がtrueを返す場合に有効です。

  • digest_auth (設定されている場合に有効)

この機能のテストでは{ "digest_auth" => "true" } のようにダミーの文字列を設定しています。

Digest認証を使うクライアントで必要なライブラリのロード

Digest認証を有効にした場合には、net/http/digest_auth モジュールをロードしようとします。

デフォルトでは有効になっていないので、モジュールを適切な方法で配置してください。

方法の1つは次のようにcouchdb.rbと同じディレクトリにnetディレクトリを配置して、相対パスでこのlibディレクトリを$:変数に含める方法です。


  bin/main.rb
  lib/
  lib/couchdb.rb
  lib/net/http/digest_auth.rb

main.rbには次のように記述します。

  $:.unshift File.join([File.dirname($0),"..","lib"])
  require 'couchdb'

方法の2つめはgemsを使い、require 'rubygems'を加える方法です。 デフォルトの場所以外にgemsでインストールした場合には次のように、その場所をポイントする必要があります。

$ gem install -i gems net-http-digest_auth

次のようなディレクトリ構造だとします。


  bin/main.rb
  lib/couchdb.rb
  lib/gems/gems/net-http-digest_auth-1.0/lib/net/http/digest_auth.rb

この場合は、main.rbの先頭は次のようになるでしょう。

  ENV['GEM_HOME'] = File.join([File.dirname($0),"..","lib","gems"])
  require 'rubygems'
  $:.unshift File.join([File.dirname($0),"..","lib"])
  require 'couchdb'

前者の方法はシンプルですがライブラリのメンテナンスを考えるならgemsの利用も検討するべきです。

それにドメイン毎の事情を考慮して、他の方法を検討することはとても素晴しいアプローチです。

SSLクライアント認証用プロパティ

stunnelを使って検証しています。

cacertが設定されている場合に、自動的にNet::HTTP#use_sslを有効にします。 それぞれに設定する値の詳細は、net/httpsのNet::HTTPクラスライブラリを参照してください。

  • cacert (デフォルト値なし。pemファイルへのパス文字列)
  • ssl_verify_mode (default: OpenSSL::SSL::VERIFY_PEER。another value: OpenSSL::SSL::VERIFY_NONE)
  • ssl_verify_depth (default: 5)
  • ssl_client_cert (default値なし。OpenSSL::X509::Certificate オブジェクト)
  • ssl_client_key (default値なし。OpenSSL::PKey::RSA オブジェクト、もしくは、OpenSSL::PKey::DSA オブジェクト)
SSLクライアント認証とBasic認証を組み合せる場合のコーディング例

n少なくとも次のような方法で各オブジェクトを準備し、Couch::Serverクラスのインスタンスを生成する事が必要です。 Basic認証が不要な場合は、user, passwordプロパティを設定しないでください。

詳細はnet/httpsのNet::HTTPクラスライブラリの各メソッドから、OpenSSL:SSLクラスライブラリと併せて参照してください。

  ssl_client_cert = OpenSSL::X509::Certificate.new(File.new(File.expand_path('stunnel.client.cert.pem', File.dirname($0))))
  ssl_client_key = OpenSSL::PKey::RSA.new(File.new(File.expand_path('stunnel.client.key.pem', File.dirname($0))), 'xxxxxxx')

  couch = Couch::Server.new("couch.example.org","5984", {
    'user' => 'admin',
    'password' => 'xxxxxxxxxx',
    'cacert' => File.expand_path('cacert.pem', File.dirname($0)),
    'ssl_client_cert' => ssl_client_cert,
    'ssl_client_key'  => ssl_client_key
  })
SSLサーバ認証について

SSLサーバ認証は、SSLクライアント認証のサブセットで、少なくともcacertプロパティにはサーバ側の証明書が検証できるPEMファイルを設定することが必要です。

その他のプロパティは任意です。

2010/11/29

CouchDB: How to use a reverse proxy server with authentication_db

Japanese edition is here.

A reverse proxy server as a front-end of couchdb seems to be useful for authentication and SSL because there are many examples, such as Apache_As_a_Reverse_Proxy.

According to the couchdb reference, however, it suggests to use the null_authentication_handler and the user=%{LA-U:REMOTE_USER} rewrite rule, it means that an local user will get the admin privilege.

A new authentication handler was developed working with an authentication_db to solve this issue.

If the reverse proxy server and couchdb are placed at different servers, this kind of handler might be useful.

The following patch is for the couchdb-1.0.1, but just added new codes. I think that it should work with another version.

This authentication handler was tested on alix with debian lenny. Some stuffs, especially erlang-R14B and couchdb-1.0.1, were manually compiled.

Setup Procedures

The diff file, 20101127.1.couchdb101.webproxy.diff, are placed at ~/, then move to the couchdb directory and apply it.

$ cd apache-couchdb-1.0.1
$ patch -p1 < ~/20101127.1.couchdb101.webproxy.diff
$ cd src/couchdb
$ make
$ sudo cp couch_httpd_auth.beam /usr/local/lib/couchdb/erlang/lib/couch-1.0.1/ebin/

To use this handler, please modify the local.ini file as following;

authentication_handers setting at local.ini

[httpd]
authentication_handlers = {couch_httpd_auth, webproxy_authentication_handler}

This handler will take two options;

  • require_authentication_db_entry (default: true) - if it's true and the authenticated user name is not on the authentication_db, then the authorization at couchdb will be failed.
  • webproxy_use_secret (default:false) - if it's true and there is no proper X-Auth-CouchDB-Token header, then the access will be denied.

The authentication_db entry is just used to get the user's role. It means just user, type and roles entries are reqruied to each document.

To enable the authentication, you need more configurations usually. Following is an typical example of the local.ini file.

Example of the local.ini file

[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 = true
webproxy_use_secret = false
secret = 329435e5e66be809a656af105f42401e

Set up Apache as A Reverse Proxy

These configurations are just an example. Please modify for your environment.

To setup apache on debian lenny, we need to create setup files on /etc/apache2/sites-enabled directory.

For port 80: /etc/apache2/sites-available/couch

<VirtualHost couch.example.org:80>
	ServerAdmin webmaster@example.org
	DocumentRoot /var/www/
	<Directory />
		Options FollowSymLinks
		AllowOverride None
	</Directory>
	ErrorLog /var/log/apache2/error.log
	LogLevel warn
	CustomLog /var/log/apache2/access.log combined
<IfModule mod_alias.c>
        Redirect permanent / https://couch.example.org/
</IfModule>
</VirtualHost>

For port 443: /etc/apache2/sites-available/couch-ssl

<IfModule mod_ssl.c>
<VirtualHost couch.example.org:443>
	ServerAdmin webmaster@example.org
	DocumentRoot /var/www/
	<Directory />
		Options FollowSymLinks
		AllowOverride None
	</Directory>
	ErrorLog /var/log/apache2/error.log
	LogLevel warn
	CustomLog /var/log/apache2/ssl_access.log combined
	SSLEngine on
	SSLCertificateFile    /etc/ssl/certs/ssl-cert-couch.pem
	SSLCertificateKeyFile /etc/ssl/private/ssl-key-couch.pem
	BrowserMatch ".*MSIE.*" \
		nokeepalive ssl-unclean-shutdown \
		downgrade-1.0 force-response-1.0
        <Location />
           AuthType Digest
           AuthName "CouchDB"
           AuthDigestDomain /
           AuthDigestProvider file
           AuthUserFile /etc/apache2/htdigest.db
           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>

To use above settings, please change that following values and files for your environment.

  • VirtualHost couch.example.org:80
  • Redirect permanent / https://couch.example.org/
  • VirtualHost couch.example.org:443
  • SSLCertificateFile /etc/ssl/certs/ssl-cert-couch.pem
  • SSLCertificateKeyFile /etc/ssl/private/ssl-key-couch.pem
  • AuthUserFile /etc/apache2/htdigest.db

To enable changes, create symbolic-links and restart apache.

$ sudo a2ensite couch
$ sudo a2ensite couch-ssl
$ sudo /etc/init.d/apache2 restart

Set up CouchDB

Possible parameters and settings are explained at the top of this document. This section explains much more details.

require_authentication_db_entry

If it's true, the authenticated username should be described on the authentication_db, such as /_users/org.couchdb.user:username.

If it's the default value, false, the authenticated user can access to the couchdb without an authentication_db entry. In this case, the role of the user set to empty, []. To enable the role, user's authentication_db entry is essential.

webproxy_use_secret

If it's true and there is no X-Auth-CouchDB-Token line at the http request header, then the authorization will be failed.

To add the X-Auth-CouchDB-Token to the request header, the following settings are required.

Modified /etc/apache2/sites-available/couch-ssl

<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>

The value of X-Auth-CouchDB-Token can be calculated by SHA1 HMAC as following;

$ 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"

The value of 'Secret' is the value of the secret key on the .ini file.

Security considerations

The default value of the webproxy_use_secret is false.

In this case, 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.

$ curl -H 'Authorization: Digest username="admin"' http://localhost:5984/_session

Please consider the webproxy_use_secret to be enable, but it's a little bit difficult, I guess. So that the default value is false to relax.

Example of a curl command line

If the ssl cert file was confirmed by the self-signed CA, then the cacert.pem file should be append to the curl command line.

$ curl --digest --cacert cacert.pem -u admin:xxxxxx https://couch.example.org/_session

Enjoy!

CouchDB: ApacheをReverse Proxyサーバにしてみた、完成版

ApacheをReverse Proxyにすると、SSL化や認証についてApacheに関する情報がそのまま流用できて便利だよね、と思い認証ハンドラを作成しました。 とりあえず自分が使うのに必要なものを作って、一般化するために最低限の機能だけを加えています。

CouchDB Wikiにある Apache_As_a_Reverse_Proxyの手順でも似たようなことはできますが、null_authentication_handlerを使う事で、localhostのユーザは無制限にadmin権限を手に入れることができてしまいます。

これはReverse Proxy ServerとCouchDBを別のサーバで動かす場合に、気をつけなければならない点です。

この認証ハンドラは強固なセキュリティを提供するものではありませんが、いくらかリスクを軽減する事ができるはずです。

最終的に仕上げたCouchDB 1.0.1用のパッチは、新しいハンドラを追加しただけで、既存の機能は変更していないので、内部的な変更がなければ基本的には他のバージョンでも動くはずです。

この認証ハンドラは AlexにDebian lennyを入れた環境で試しています。 Erlang-R14BとCouchDB-1.0.1は手動でコンパイルしました。

セットアップ手順

~/20101127.1.couchdb101.webproxy.diffを準備して、CouchDB-1.0.1をコンパイルしたディレクトリに移動します。

$ cd apache-couchdb-1.0.1
$ patch -p1 < ~/20101127.1.couchdb101.webproxy.diff
$ cd src/couchdb
$ make
$ sudo cp couch_httpd_auth.beam /usr/local/lib/couchdb/erlang/lib/couch-1.0.1/ebin/

local.iniなどのiniファイルで認証にwebproxy_authentication_handlerを使用するようにします。

local.iniに追加する設定

[httpd]
authentication_handlers = {couch_httpd_auth, webproxy_authentication_handler}

この認証ハンドラは2つのオプションを取ります。

  • require_authentication_db_entry (default: true) - trueの場合、authentication_dbにユーザ名に対応したエントリがない場合、認証に失敗します
  • webproxy_use_secret (default:false) - trueの場合、Reverse Proxyが適切なX-Auth-CouchDB-Tokenヘッダを付与しない場合、認証に失敗します

これらを加えた一般的なこの認証ハンドラを使う場合の設定は次のようになります。

一般的な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 = true
webproxy_use_secret = false
secret = 329435e5e66be809a656af105f42401e

Reverse Proxy側の設定 (Apache)

Debianでは/etc/apache2/sites-enabledディレクトリに置いた設定ファイルをhttpdが認識します。 まずは/etc/apache2/sites-enabledにファイルを作成します。

Port 80用設定ファイル: /etc/apache2/sites-available/couch

<VirtualHost couch.example.org:80>
	ServerAdmin webmaster@example.org
	DocumentRoot /var/www/
	<Directory />
		Options FollowSymLinks
		AllowOverride None
	</Directory>
	ErrorLog /var/log/apache2/error.log
	LogLevel warn
	CustomLog /var/log/apache2/access.log combined
<IfModule mod_alias.c>
        Redirect permanent / https://couch.example.org/
</IfModule>
</VirtualHost>

Port 443用設定ファイル: /etc/apache2/sites-available/couch-ssl

<IfModule mod_ssl.c>
<VirtualHost couch.example.org:443>
	ServerAdmin webmaster@example.org
	DocumentRoot /var/www/
	<Directory />
		Options FollowSymLinks
		AllowOverride None
	</Directory>
	ErrorLog /var/log/apache2/error.log
	LogLevel warn
	CustomLog /var/log/apache2/ssl_access.log combined
	SSLEngine on
	SSLCertificateFile    /etc/ssl/certs/ssl-cert-couch.pem
	SSLCertificateKeyFile /etc/ssl/private/ssl-key-couch.pem
	BrowserMatch ".*MSIE.*" \
		nokeepalive ssl-unclean-shutdown \
		downgrade-1.0 force-response-1.0
        <Location />
           AuthType Digest
           AuthName "CouchDB"
           AuthDigestDomain /
           AuthDigestProvider file
           AuthUserFile /etc/apache2/htdigest.db
           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>

サーバ名やファイル名など、具体的には次の内容が適切か確認してください。

  • VirtualHost couch.example.org:80
  • Redirect permanent / https://couch.example.org/
  • VirtualHost couch.example.org:443
  • SSLCertificateFile /etc/ssl/certs/ssl-cert-couch.pem
  • SSLCertificateKeyFile /etc/ssl/private/ssl-key-couch.pem
  • AuthUserFile /etc/apache2/htdigest.db

ファイル名を指定して、a2ensiteコマンドで設定を有効にします。

$ sudo a2ensite couch
$ sudo a2ensite couch-ssl
$ sudo /etc/init.d/apache2 restart

CouchDB側の設定

設定自体は冒頭に説明しましたが、その内容について解説します。

require_authentication_db_entryについて

trueの場合、authenticate_db(default: '_users')にユーザ名(example: username)に対応する文書(example: /_users/org.couchdb.user:username)がない場合、認証に失敗します。

require_authentication_db_entryを"false"にすると対応する文書がなくてもアクセス可能となります。 その場合は文書があればroleが設定され、なければroleは空"[]"になります。

webproxy_use_secretについて

trueの場合、HTTP Request Headerに適切なX-Auth-CouchDB-Token行がない場合、認証に失敗します。

変更したIfModule mod_proxy.cの内容

<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>

X-Auth-CouchDB-Tokenに指定する値はSHA1のHMACで、計算方法は 以前に投稿したように、salt = 329435e5e66be809a656af105f42401eとして次の手順で行ないます。

$ 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"

webproxy_use_secretがデフォルトのままfalseの場合、ローカルユーザがdummyのAuthorization行を追加した場合に、認証に成功してしまいます。

$ curl -H 'Authorization: Digest username="admin"' http://localhost:5984/_session

セキュリティの観点から設定をお勧めしますが、設定が少し難しいので、デフォルトの値をfalseとしています。

CouchDBは使えるのが基本ですからね。

couchdbをリスタートして設定を反映させます。

$ sudo /etc/init.d/couchdb restart

稼働確認

SSLに自己認証CA局を使っている場合は、そのcacert.pemファイルを指定して他のホストから次のようなリクエストを投げてみます。

$ curl --digest --cacert cacert.pem -u admin:xxxxxx https://couch.example.org/_session

2010/11/26

CouchDB: ApacheをReverse Proxyサーバにしてみた、その後

前回投稿した記事で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.

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.

2010/11/25

松島x町山 未公開映画祭: 「ジーザス・キャンプ」をみた感想

関東圏で放送されているアメリカのドキュメンタリー映画を紹介するTOKYO MXの番組で紹介された映画が オンラインでみる事ができるようになったので、英語の勉強を兼ねて何本か観ました。

その中で最初に紹介されている番組で、かつ、強烈な印象を残す「ジーザス・キャンプ」をみた感想をメモしておきます。

宗教に対するスタンスとこの映画を取り上げた動機

ここでは個人的な宗教観を元に何かいうつもりはないのですが、切り離す事もできないので、まとめておきます。

私は自然への畏怖の念とか、他人の信仰に対する敬意は持ちますが、現在の私は無宗教と形容するのが適当だと思います。

かといって宗教と無縁かというと、幼稚園から小学校卒業くらいまでの間は伝統的なプロテスタント系の教会とのかかわりがありました。

穏健な環境で、今回取り上げた映画のように、聖書を絶対視するような事は求められず居心地はよかったのですが、引越しを機会に自然と疎遠になりました。

何かを絶対視したいがために、客観的な事実をねじまげかねないような信仰は私の価値観と両立しません。 その点がこの映画を取り上げた理由だと感じています。

さて、感想のまとめ

この映画をみて「嗜癖」、「共依存」という言葉が頭をよぎりました。

嗜癖、共依存の一つの考え方

A・W・シェフの著書「嗜癖する社会」の監訳者まえがきの中で斎藤学さんは、シェフが定義した3つのシステムを次のように簡単にまとめています。

  • 白人男性システム - 「…自分や他人をコントロールするパワーを基本とし、そこから生じる力の階層の存在を信じるシステム」
  • 反応性女性システム -「白人男性システムに奉仕しようとする共依存的態度が生まれてくる。」、あるいは、「自分自身の能力を高めて弱者のレッテルを隠し、白人男性システムのメンバーになりおおせようとする」システム
  • 第三のシステム - この2つから独立した「価値観やルールにとらわれず、自分の性や資質に忠実に生きようとする人々の作る関係」から成るシステム

ポイントは私たちの生きるこの社会が「自分や他人をコントロールするパワーを基本とし」ていると規定する部分です。

映画を宗教とは違う角度からみてみる

神という絶対的存在には無限のパワーがあると考えれば、その無限のパワーを元手に、自分にも転嫁しようとする欲求は、ごくごく自然に思えます。 無限のパワーがあるなら使わなきゃ損でしょう、そんな感じです。

また神の言葉を語る事で、本来の脆弱な自分を覆い隠すことができます。 自分自身のパワー不足を補い、他人にパワーを行使することが可能な存在になるわけです。

映画の中では説教をする時の自分を、自分であって自分ではない存在と認識しています。

子供たちも大人たちも、パワーを指向するシステムの中で信仰を強化するインセンティブを持っている、そういう存在にみえました。

彼らが第三のシステムに存在していれば良いのですが、このシステムはシェフによって前述の定義に加えて「多様で変化し、オープンなシステムの輪の中でとらえられ」るものとされています。 彼らは何かには忠実ですが、それは「自分の性や資質」ではなく、第三のシステムに当てはめることには無理があります。

教条主義的な環境で子供が育つとどうなるんだろう

私は特定の強固な信念を持って疑う事を良しとしない教条主義的な環境で子供が育つ事で、特に思春期の迷いや葛藤を乗り越えるのではなく、迂回してしまい精神的に成長できずに大人になる可能性を心配しています。

いたずらに常に自己を肯定させ、葛藤する事を避ける人達もまた第三のシステムに入れるには、柔軟性が欠けていると思っていますが、いまは映画に出てきた人達を考えます。

この映画に出てくる大人たちをみていると、子供を思いどおりに動かしながらも、適当に楽しんでいるようなので、あまり心配はなさそうです。

しかし、その高みにまで到達できない、この映画には出てこない、素直が故に脱落していく子供たちがいるように思えてなりません。

この固定化した観念の中で折り合いをつけていくには常に正当化や全肯定など、ごまかす事が必要になるでしょう。

信仰によって人間の弱い部分を強化してくれるのは、多くの宗教に共通した側面だとは思います。 しかし映画に出てくる人達は自分自身の前に他人を屈服させる、あるいは王になる瞬間(あるいは王の列席に引き上げられること)を待ち望んでいるようにしかみえないのは私だけでしょうか。

あと気になったところは…

自分の欲求を満したいがために、やたら言葉の前に「イェスの御名によって」とつけているのは目立つところです。

自己の力を誇示したいだけなんじゃないかなって感じるんですよね。

でもそう思うのは、きっと過去の自分の影をいくらかそこにみているんでしょう。

嗜癖しない人などいないし、他人に影響力を持たずにいられるわけもありません。 これは程度(バランス)と、現状を変えていけるFreeな状態にあるかどうかの問題です。

若い人たちには一貫した力強さは魅力的かもしれない。 私みたいにいう人間を一貫性のない頼りないものと思うのかもしれません。

けれど柔軟に自分を変えていく力を一番持っている世代が、自分と異なる考えを、自分の信念だけを根拠に否定している様はみていて嫌な気分になりました。

穏やかなルールと多様性が繁栄につながる、これは私の信念で説明することは困難ですが、悪くないと思っています。

この記事で取り上げた品々

2010/11/24

CouchDB: SSLクライアント認証は必須なのか

クライアント認証は必須ではない

以前書いた「 CouchDBのSecurity Featuresについて」では、"database readers"(Readers)権限設定にUserやRolesを何か設定しないと、誰でもReaders権限を持つことになると書きました。

このReaders権限は通常のDocumentを操作することが可能で、閲覧する他に作成、編集ができます(削除は不可)。 Anonymousに文書を編集されても困るので、この点で(主にBasic認証での)UserかRolesの設定は必須といえるでしょう。

さらに別の記事ではSSLのクライアント認証を行なう事で、証明書を持つユーザだけのアクセスが可能になるとも書きました。しかしクライアント認証だけではUserやRoleは設定されません。

SSLを利用するもう一つの理由は、通信経路が暗号化されないために、Basic認証などに使うID/Password情報がネットワークに平文で流れることを避けることにあります。

デスクトップ環境の中でも、特定の個人だけがログインできると保証されている場合を除けば、何らかの認証は必須といえるでしょう。 また、外部サーバ、ユーザとの連携があれば、SSLによる経路の暗号化が必要になります。

結論としては、SSLを使う主な理由は経路の暗号化で、クライアント認証は実験的な要素が強く、必須とはいえません。

ただSSLクライアント認証は、もう少し使われても良いとは思います。 ネックになるのはCA局の運営コストや外部委託した場合のコストがかなり大きそうだというところでしょうか。

クライアント認証を使うポイントを無理やり考える

ネットワークを移動する相手を信用したい場合には、クライアント認証は有効です。 また外部から観察した情報だけで破られるID,Passwordとは違い、デジタルとはいえファイルに関連付けられた認証情報が必要な点も利点の一つでしょう。

サーバ間認証

ところで最初に試したProxy認証(のような外部認証サーバ)と組み合せたサーバ間認証に使う場合はどうでしょう。

Proxy認証ではMAC周りが弱点で、経路の盗聴に対しては脆弱です。 外部認証サーバ自体はProxy認証よりはましな仕組みでユーザとのやりとりをするでしょうから、外部認証サーバとCouchDBとの間にSSLクライアント認証をいれれば、経路の暗号化しながら望む相手とだけ通信をする事が可能です。

もっとも経路を暗号化すれば、クライアント認証にこだわる必要はなくて、外部認証サーバのIPアドレスでアクセス制御をしても同じ程度の要件は満しそうです。

レプリケーション

YouTubeなどをみているとCouchDBのプレゼンテーションの中では、Offline DBとしての使い方が想定されています。

この場合は、Stunnelのクライアント機能を使ってポートを開けば、アプリケーションレベルでは変更なしに透過的にネットワークを隔てた相手方に接続することができます。 ただし、同一サーバのユーザは誰でもその経路に接続できることになります。

これが許容できれば別ですが、レプリケーション機能自体が認証の仕組みを持たないと、なかなかセキュリティを高めることはできなさそうです。

ここについてはSSLクライアント認証を含めた認証機構は有望そうです。

何が必要なのか

レプリケーションのところはちょっと手がでないので、一般ユーザがCouchDBに接続する場合を考えます。

そもそものSSLクライアント認証を使おうとした動機は、CouchDBの組み込みの仕組みが既にあるLDAPやらのID, Passwordの仕組みと一緒に使えない事に起因するものでした。

パスワードをいく通りも覚えるのは無理ですから、普通はLDAPに統合するでしょう。 現実世界がなかなかこうなっていないのは理解していますが、そうするべきです。

LDAPでも何でも生のパスワード情報にアクセスするのは容易ではありません。 バッチ的な手法を使っても、CouchDB上にユーザ毎のページを生成するのは難しい側面があります。

解説記事を盲信することなかれ

CouchDB Wikiや解説記事の中にはApacheによる認証を行う方法を紹介しているものがあります。 authentication_handlerにnull_authentication_handlerを指定するよう指示がありますが、全てのユーザに"admin" Roleを付与することに注意してください。

とはいえCouchDB附属のBasic認証では、LDAP認証も組み込まれていないですし、いろいろな要件に合わないのは前節のとおりです。

企業での利用にはProxy認証の機能を参考に、Apache + null_authentication_handlerよりはましな仕組みの構築を検討してみるべきだろうと思います。

Proxy認証に代わる仕組みとは…!?

いわゆるCGIでいうところのREMOTE_USER変数を利用して、/_users/org.couchdb.user:$REMOTE_USERからRoleを自動的に設定する仕組みがあれば、ApacheやNginxをSSL、認証のプロキシーとして使えそうな気がします。

実際にはAuthorizationヘッダ行をどうにかするんでしょうけど、試してみる価値はありそうです。

erlangでプログラミングしたことはないけど、自分にできるものか試してみることにしましょう。

CouchDB: Prismを使ってSSLクライアント認証専用のブラウザを作ってみた

Prism 1.0b4を使って、SSLクライアント認証を有効にしてCouchDB専用のブラウザを作成してみました。

まずSSLクライアント認証を有効にするために、newcert.pemとnewkey.pemの内容をPrismに取り込む必要があります。

newkeym.pemの内容を取り込むために、PKCS12形式に変換します。 そこでいつも使っているPEM形式からPKCS12形式のファイルを作成するところから作業を始めます。

またFirefoxでも同じ方法でクライアント認証を有効にできます。 しかし普段使っているFirefoxに自己認証CA局のcacert.pem等を登録したくなかったため、プロファイルが独立しているPrismを利用しています。

プロファイルを新たに作成したFirefox環境でも同じ事はできるはずですが、プロファイルの選択は扱いずらいところもPrismを選択した理由です。

作業環境

いつもどおりUbuntu 10.04 LTS amd64上で作業を行ないます。 試していませんが、ディレクトリへのパスが違うところをカバーすれば、他のプラットフォームでも問題なく動くでしょう。

またいくつかのファイルは次の場所以下にあるとしています。 適当に読み替えてください。

  • prism-1.0b4の展開先: ~/work/prism-1.0b4/
  • demoCAディレクトリ: ~/lib/demoCA/

PKCS12形式のファイルを準備する

この作業は以前に作成したstunnelに接続するためのPEM形式のファイルが手元にあるところから始めます。

カレントディレクトリにnewcert.pemとnewkey.pemの2つのファイルがあるとします。

$ ls -l newcert.pem newkey.pem
-rw-r--r-- 1 user1 user1 3494 2010-11-15 15:05 newcert.pem
-rw-r--r-- 1 user1 user1  951 2010-11-15 15:04 newkey.pem

newcert.pemファイルの先頭はテキストの説明文が入っています。

Certificate:
    Data:
        Version: 3 (0x2)
...
-----BEGIN CERTIFICATE-----
MIIDWjCCAsOgAwIBAgIJANXx3xE0zyuGMA0GCSqGSIb3DQEBBQUAMIGWMQswCQYD
...
-----END CERTIFICATE-----

newkey.pemファイルの先頭はこんな感じです。

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,8EE3D06ADB9C1FD9

gvHfg80xquvNmLpmajEU9DwEoxOHk+TwaNQQqEqfVorciDO3KdLGdwSbKshowhGi
QjiWBeHC9fOuzHhUfj5pPCuEvO7ImoKaOgia9Zj9IiEg37t4J8Rl2bjxpcdN73HB
...
-----END RSA PRIVATE KEY-----

このファイルからPKCS12形式のファイルを生成します。 とりあえずファイル名は newcertkeys.p12としておきます。

コマンドラインは次のようになってCA局のcacert.pemに"../demoCA/cacert.pem"でアクセスしています。 "name"は何でも良いのですが、証明書のCommonName (CN)と同じにしておきます。 ここら辺は環境に合わせて変更してください。

クライアント証明書のCNをemail addressにするのは良くある使い方の一つだと思います。

$ cd ~/lib/demoCA.couchdb_stunnel_client_key
$ openssl pkcs12 -in newcert.pem -inkey newkey.pem -certfile ../demoCA/cacert.pem -out newcertkeys.p12 -export -name "user1@example.org"
Enter pass phrase for newkey.pem:  udfa93a
Enter Export Password:  re89efkjsadf
Verifying - Enter Export Password:  re89efkjsadf

最初に入力するのはnewreq.pemを作成する段階で入力したパスフレーズで、次に入力する"Enter Export Password"の部分は今回新たに決めるところです。

ひとまずファイルを生成したので、Prismに移ります。

Prism 1.0b4を使ってStunnelに接続するWebブラウザを生成する

PrismはMozilla Labsが開発している、任意のWebアプリケーション用のWebブラウザを生成するアプリケーションです。

Prismの特徴

デフォルトでは、このWebブラウザにはメニューバーやロケーションバーやブックマークバーといった、不特定多数のWebサイトを閲覧するための便利な機能はありません。

Prismは、Firefoxによく似た、しかし機能が制限されたWebブラウザを作り出します。

Prism自身はWebブラウザではなく、起動するとURLの入力を求め、PrismはそのURLを開くWebブラウザが起動するよう、デスクトップにショートカットやらスクリプトファイルを作成します。

つまりPrismを使う事でTwitter Webインタフェースの専用クライアントや、Eijiroを検索するための専用クライアントを準備することができます。

この利点はFirefoxでいうところのプロファイルをURL毎に作成し、さらにプロセスを独立させ、互いの干渉を避けるところにあります。

PrismにSSLクライアント認証用のURLを登録する

1.0b4を展開したディレクトリに移動し、prismを起動します。

$ cd prism-1.0b4
$ ./prism

ここで入力した内容は次のとおりです。

  • URL: https://home.example.org:5984/_utils/
  • Name: CouchDB_home
  • Show navigation bar: チェック
  • Show status messages and progress: チェック
  • Enable navigation keys: チェック
  • Desktop: チェック
  • Icon: 指定なし

この作業の中でPrismとそこから起動したWebブラウザは"https://home.example.org:5984/_utils/"に接続しようとしますが、まだクライアント認証ができないためエラーになります。

途中でポップアップが表示されたらCancelするなどしてやりすごして入力します。

URLの注意事項

ここで入力するURLを https://home.example.org:5984/_utils と最後の'/'を取ってしまうと、SSLではなくhttpで接続させるリダイレクトに失敗してエラー画面になってしまうので注意してください。

作業ディレクトリへの移動

PrismでSSLの証明書関連の作業をするためには、プロファイルディレクトリに移動します。

Prismによって作成されるディレクトリは2つあります。 サブディレクトリ名は最初に登録した名前(Name:)を小文字にして空白や起動を'_'に変換した名前を使っています。

  • ~/.prism/couchdb_home/ - プロファイルやFirefox的なファイルが配置される
  • ~/.webapp/couchdb_home@prism.app/ - 最初に開くURL等が記述された設定ファイル、Icon用PNGファイルの格納先

今回は~/.prism/以下のディレクトリで作業をします。 適当な名前のプロファイル用ディレクトリを探して、その中に移動しています。

$ cd ~/.prism/couchdb_home
$ ls
55good1m.default  profiles.ini
$ cd 55good1m.default
$ ls 
Cache      compatibility.ini  extensions        localstore.json  permissions.sqlite     pluginreg.dat
XPC.mfasl  compreg.dat        extensions.cache  localstore.rdf   places.sqlite          prefs.js
XUL.mfasl  cookies.sqlite     extensions.ini    mimeTypes.rdf    places.sqlite-journal  xpti.dat

作業をすると、このディレクトリにkey3.dbやcert8.dbの名前でファイルが作成されていきます。

作業に使うcertutilコマンドの説明は「 Using the Certificate Database Tool」にあります。

opensslコマンドと似たような事をしますが、その背景や概要についてはSSL Referenceの「Chapter 2. Getting Started With SSL」にあります。

自己認証CA局のcacert.pemを登録する

まずはクライアント認証の前にstunnelが使用するstunnel.pemを検証するため、対応するcacert.pemを登録します。

$ ~/work/prism-1.0b4/xulrunner/certutil -A -n demoCA.example.org -t "Cu" -d . -i ~/lib/demoCA/cacert.pem

内容を確認する場合には次の要領で行ないます。

$ ~/work/prism-1.0b4/xulrunner/certutil -L -d .
Certificate Nickname                                         Trust Attributes
                                                             SSL,S/MIME,JAR/XPI

demoCA.example.org                                           C,,  
クライアント証明書の登録

続いてクライアント証明書を登録します。 ここではPKCS12形式に変換したファイルを利用します。

$  ~/work/prism-1.0b4/xulrunner/pk12util -d . -i ~/lib/demoCA.couchdb_stunnel_client_keys/newcertkeys.p12 

keyファイルの内容を保護するためにパスワードを聞かれるので、適当に決めます。

最後にnewcertkeys.p12ファイルを開くためのパスワードを入力するので、先ほど決めたパスフレーズを入力します。

Enter a password which will be used to encrypt your keys.
The password should be at least 8 characters long,
and should contain at least one non-alphabetic character.

Enter new password:   eu938lakj
Re-enter password:   eu938lakj
Enter password for PKCS12 file:   re89efkjsadf
pk12util: PKCS12 IMPORT SUCCESSFUL

作業が終ったら先ほどと同じように登録された鍵ファイルの情報を確認しておきます。

$ ~/work/prism-1.0b4/xulrunner/certutil -L -d .
Certificate Nickname                                         Trust Attributes
                                                             SSL,S/MIME,JAR/XPI

demoCA.example.org                                           C,,  
user1@example.org                                            u,u,u

ここで u,u,uとなっているのが、keyファイルが読み込まれたことを示しています。

このままではcertファイルがまだ組み込まれていないので、それを読み込みましょう。

$ ~/work/prism-1.0b4/xulrunner/certutil -A -n "user1@example.org" -t "T,," -d . -i ~/lib/demoCA.couchdb_stunnel_client_keys/newcert.pem

内容を確認して、 Tu,u,uと先頭にTが付いたことを確認しておきます。

$ ~/work/prism-1.0b4/xulrunner/certutil -L -d .
Certificate Nickname                                         Trust Attributes
                                                             SSL,S/MIME,JAR/XPI

demoCA.example.org                                           C,,  
user1@example.org                                            Tu,u,u

クライアントの起動

この起動方法はいろいろですが、Linuxの場合はデスクトップにあるショートカットの内容を元にコマンドラインから起動することも簡単にできます。

$ "/home/user1/work/prism-1.0b4/prism" -override "/home/user1/.webapps/couchdb_home@prism.app/override.ini" -webapp couchdb_home@prism.app

起動時に送信するクライアント認証のために送信する証明書を選択するダイアログが表示されます。

Webブラウザ起動時に表示されるダイアログ

選択した証明書に対応するKeyはパスワードで保護されているため、PKCS12ファイルをインポートする時に入力したパスワード( eu938lakj)を入力します。

keyにアクセスする為のパスワード入力ダイアログ

最後にBasic認証用のパスワード入力ダイアログが表示されて、CouchDBのWebイタンフェース(futon)に接続できれば完了です。

Prismを起動しても表示が正しく行なわれない

ここまででPrismの準備は終っていますが、このままBasic認証を有効にしたCouchDBにアクセスすると、SSLの有無に関わらず、DBやDocumentのリストが表示されません。

Basic認証を使っていないと気がつかないかもしれませんが、これはPrismで使用しているJavaScript Engineの挙動によるものです。

Error Consoleを開くとfuton.jsの中でwindow.nameがJSON.parse()で処理できない値を返していることが問題だという事がわかります。

Error Consoleに表示されたwindow.name関連のメッセージ

これは 既に報告されていて、 パッチ(ouchdb-896.patch)が提供されています。

簡単なパッチなので手で当てて終りましたが、Webブラウザ側ではJavaScriptがCacheされているとなかなか再読み込みされないかもしれません。

JavaScriptを更新する場合はプロファイルの下にあるCacheディレクトリを削除します。 今回の場合は、~/.prism/couchdb_home/55good1m.default/Cache を削除して解決しました。

さいごに

ネタはCouchDBとStunnelでしたが、Firefox系のSSLクライアント認証の方法としてはごくごく普通のことをしたと思います。

Prismは便利なんですけれど、自分でショートカットを削除しても復活できるところが、場合によっては便利だったり、問題だったりしそうです。

その点ではPrismのProfile Managerは役に立たないので、管理用のユーティリティを作ってみてもいいのかもしれません。

2010/11/19

サーバ側にcacert.pemを設定する理由を自分なりにまとめてみた

Transport Layer Security(TLS - RFC2246 :: 日本語訳)はSSLの後継ですが、実際にはむしろSSLという言葉をTLSの意味で使う事が多いと思います。 SSL v2をデフォルトで無効にする実装は増えていますが、とりあえずはSSLとTLSは区別なく使うことにします。

また基本的に自己認証CA局とサーバ認証書を利用する前提で書いています。

ここに至る経緯

サーバアプリケーションの設定をしていると、たいていSSLの設定項目はオプションですが、stunnelのように基本機能に組み込まれている場合はパッケージがPEMファイルを生成してくれたり、ejabberdのように(ほぼ)標準機能としてSSLを利用するサーバアプリケーションは(関心とともに)増えている印象です。

ejabberdの場合は混乱もなさそうですが、パッケージの導入時に生成されたPEMファイルはサーバ名の変更などに追従できずに混乱を招く場面も考えられます。

OpenSSLを利用していても設定方法はサーバアプリケーション毎に違い、SSLはいろいろ自分で混乱するところなので、自分の理解している範囲をまとめておこうと思いました。

SSL関連の設定項目への疑問

OpenSSLはいろいろなプログラミング言語でバインディングライブラリが用意されていますが、大抵はプログラマがOpenSSLを理解している前提でC APIへのマッピングだけが記述されていて、使い方とか最低限必要な項目なんかは分からないものだという印象を持っています。

サーバ管理者になると、バインディングライブラリ設計者に加えて、さらにプログラマの実装を1段挟んで設定ファイルと格闘することになるわけです。

アプリケーションでのSSLの設定手順は各ガイドの作者によって微妙に違っていて、他のアプリケーションでの知識/経験との連携が難しいのかもしれません。

いろいろ調べているとサーバアプリケーションを設定する際の疑問で、「なんでサーバ側でcacert.pemファイルを設定しなきゃいけないんだ」、「この項目は必要なの?」という発言を目にする機会がありました。

Certificate Revocation List (CRL)とかの扱いはサイト毎のポリシーの問題ですが、「これ設定必要なの?」みたいな疑問を持ってしまうと、いろいろある設定項目に手を出してはまってしまうかもしれません。

SSL証明書を管理するベストな方法は?

サーバアプリケーションを使うためにマニュアルやガイドを確認して、意味もわからずに手順通りに文字列を打ち込んで生成されたファイルを言われるがままにサーバに配置したりするのが実情なのかもしれません。

であれば、なおさら何かソフトウェアを利用したところで、そのソフトウェアの手順に書かれていない事や、そのソフトウェアの対応していないアプリケーションを利用すると、とたんに混乱しそうです。

これを読めば完璧というものもなさそうですが、基本的なところを理解しておけば混乱を少なくする事はできそうな気がします。

TLS/SSLの基本的な処理フロー

RFC 2246( 日本語訳)によれば、次の図のように、通信を開始するまでにサーバ証明書(Server Certificate)の送信と、オプションであるクライアント証明書の送信が行なわれます。

クライアント、サーバ間のSSL通信を開始する際のやり取り

ここでのポイントはクライアント証明書の送信は任意である点で、サーバは特定のクライアントとだけ通信を行ないたい場合にクライアント証明証を要求します。

後述するように、通信のためには、サーバは自身の証明書の出所を検証する必要はありませんから、サーバ側が持つcacertはクライアントから証明書が送信された場合にだけ、そのクライアント証明書を確認するために使用されます。

クライアントの送付してくる証明書によってサーバ側が持つべきcacertは変化しますから、もし不特定多数のクライアントとのクライアント認証をすると考えると、サーバ側は世の中にある中間CA局を含めたcacertを収集しなければいけなくなるという事です。

クライアント認証はID/Passwordの代りになるか

最終的には、用途や役割をよく考えて、ロールプレイなどによってより適切な利用方法を検討することになります。

基本的にクライアント証明書はファイルという電子化されている物証によって個人を特定する事ができますから、記憶に依存するID/Passwordによるログイン認証と、性質は違いますが、同じ役割を果します。

クライアント証明書は数年という単位で流通して、一時的なアクセス制限を加えるには不向きです。 CLR配布の問題は常にありますし、サーバ側で証明書を許可対象から外すのも手間であったり確実性に欠けます。

この点ではLDAPなどにより集中管理されたID/Passwordがより向いているでしょう。

しかしどちらが常に優位であるという事はありません。

もしアクセスが特定のネットワーク内部で、バッチ的なプログラムから行なわれる場合には、大抵のケースではクライアント証明書のみによるアクセス制限がメンテナンスと堅牢性のバランスから選ばれると思います。

その反面、人間によるインタラクティブなアクセスに対しては、ID/Passwordによる認証の方が、ユーザの教育という観点からも、適切な場面が多いと思います。

さらにユーザが不特定な場所(インターネット)からアクセスしてくる場合に、クライアント証明書の配布を加えて行なう事は現実的な対応です。

とはいえ認証(Authentication)と承認(Authorization)の問題を含めて、アプリケーションが提供する機能が不足していれば、その制限を考慮しなければいけません。

認証と承認の問題は別のトピックなので、元に戻して証明書ファイルの扱いについて、まとめていきます。

cacertとcertとkeyファイル

通信に使われる情報として、CLRなどの付加的なファイルを除いて、最低限必要な組み合せという点では (通信相手の証明書を発行した)CA局のcacertcertkeyの3つの情報があります。

certとkeyファイルを生成するrequestファイルは作業が終ったら削除しましょう。

ポイントはクライアントやサーバが持つcacertの情報は、通信の相手方の証明書を発行した局の情報だという点です。

自分の理解をまとめると次のような図になりました。

CAが生成するファイルが通信に使われる様子

ポイントはサーバは認証自体にはクライアントから送られてきたclient.cert.pemとそれを発行したcacert#1.pemを使いますが、実際には全ての通信を許可するわけではないので、承認(Authorization)には、そのclient.cert.pem自体を別途許可リストとして登録しておく必要があります。

stunnelでは図のcaert#1.pemとclient.cert.pemの情報を"-a"オプションに指定するディレクトリに入れて、c_rehash <directory>を実行することで、サーバに登録する事になります。

自分の理解をまとめると、だいたいこんな感じです。

CA局の信頼性について

世の中には自己認証によるCA局ファイルを配布しているグループがあります。

遊びとして興味はそそられますが、そのcacertを自分のブラウザの許可リストに入れておこうとは思いません。

あるいはopenca.orgがOpen CAアプリのデモで動かしているCA局が発行する鍵ファイルをそのまま使うのは間違った行為です。

遊びとして相応しい対応は自分でPKIを構築する事で、他人と(別の他人が作った)rootCAを共有することではありません。 rootCAの共有は自分の家の鍵を他人に渡すようなもので、そのリスクが受容できる場合には、なかなかおもしろい経験になるでしょう。

鍵ファイルが高いという人や、無料のCAを使おうという人もいるけれど…

自己認証による(プライベート)CA局の運営は 利害の一致する各個人や組織の中で行なわれるべきものです。 これは鍵ファイルを買いましょう、という話ではありません。 むしろ適切な場面では、プライベートCAの運営を行なってノウハウを蓄積するべきです。

しかしイントラネットのサーバに社内にきた外部の人間がPCを繋いでアクセスするのであれば、そのWebサーバには商用CA局が発行する鍵ファイルを使うべきです。

あるいは組織が肥大化してrootCAが2つ以上存在するのであれば、そういうガバナンスの存在しない場面では、商用CA局の発行する鍵ファイルや、プライベートCAのアウトソーシングを使うべきでしょう。

そうでない場合、社内で配布するPCに自社で運用するCA局のcacertを含めて配布するのは悪いアイデアではありません。

けれども、プライベートCAの認証する末端のデバイスや個人が自分と利害の対立するものでない事を確認してください。 これは、そのCA局の運営方法(むやみに証明書を発行しない、証明書を発行するPCは一つだけ、等)も含めて考えるという事です。

職権の分離に応じて中間CA局を設けて、必要な範囲にのみその中間CAの配布する鍵ファイルと、デバイスにcacertを配布するという使い方はより望ましいでしょう。

結局、この問題には銀の弾丸は無くて、状況をいろいろ考慮して柔軟に解決するべき問題だとしかいえないんですけどね。だから論争(というよりも口論)にもなるんでしょうけれど。

2010/11/16

クライアント認証を有効にしたstunnelに接続するCouchDB用Rubyクライアントを 作ってみた

前回までで、stunnelを起動したものの接続テストがまだでした。 元々別件で使用した CouchDB Ruby用ガイドのCouchモジュールをSSLに対応させてみました。

クライアント認証は、サーバ(今回はstunnel)にクライアントの持っているCertificateを認めさせる事なので、クライアント用にCertificateを作成して、そのCA局情報をstunnelに登録しておきます。

ついでにRubyスクリプト側にcacertファイルを持たせて、サーバ認証もするようにしました。

SSLのサーバ認証とクライアント認証で必要なこと

今回の作業で stunnelのFAQやら RubyのNet::Httpsマニュアルやら、その他のドキュメントを参考にしました。

Rubyのマニュアルは素晴しい内容だと思いましたが、大抵のドキュメントはサーバ認証に必要なことと、クライアント認証に必要なことを、あまり分けて書いていないか、説明していない印象を受けました。

前回のStunnel用のPEMファイルを準備したのだって、ほとんどサーバ認証用に必要な作業です。

サーバ認証の流れ

今回はサーバ認証のためにcacert.pemをRubyスクリプトにも配置しますが、その作業を先にします。

  • サーバ(stunnel)側にnewcert.pemとnewkey.pemを埋め込む (既にstunnel.pemとして完了済み)
  • クライアント側にサーバのnewcert.pemに対応したcacert.pemファイルを配置する
  • クライアントが動作し、手元のcacert.pemと送られてきたnewcert.pemの内容を確認した上で、Common Nameが接続に使用したサーバ名と一致するか確認する

変更個所は2点で、sslを使うためにrequireで"net/https"を指定します。

Couchモジュールファイル先頭のrequire行の変更個所

require 'net/https'

次にCouchモジュールのrequestメソッドを書き換えました。 今回はBasic認証周りの変更は外してあります。

書き換えたCouchモジュールのrequestメソッド

    def request(req)
      client = Net::HTTP.new(@host, @port)
      if @options.kind_of?(Hash) and @options.has_key?('cacert')
        client.use_ssl = true
        client.ca_file = @options['cacert']
        client.verify_mode  = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
        client.verify_mode  = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
        client.verify_depth = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_depth')
        client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
      end

      res = client.start { |http| http.request(req) }
      unless res.kind_of?(Net::HTTPSuccess)
        handle_error(req, res)
      end
      res
    end

Rubyスクリプトから使う場合には、スクリプトと同じディレクトリに cacert.pemファイルを配置して、インスタンスメソッドの第3引数でHashオブジェクトを指定します。

変更したCouchモジュールを使うRubyスクリプトの抜粋

couch = Couch::Server.new("home.example.org","5984", {
  'cacert' => File.expand_path('cacert.pem', File.dirname($0))})

これでサーバ認証用の設定は終りです。cacert.pemをクライアント側のRubyスクリプトに読み込ませたのはサーバ認証のためでクライアント認証には反対にサーバ側にcacert.pemを配置します。

サーバ側に配置するcacert.pemファイルは、クライアント用に作成したnewcert.pemに対応するものであるのが後続作業のポイントになります。

クライアント認証用のPEMファイルの作成と配置

まずは作業内容の説明と準備

今回はサーバ用Certificate(newcert.pemとnewkey.pem)を作成する時に使用したのと、同じdemoCAを使います。

とはいえ、demoCAは同じでなくても構いません。 この先の作業に使うdemoCAは1つですから、クライアント用PEMファイルのdemoCAの事だと思って読んでください。

作業自体は「 Ubuntu 8.04 LTS上のApache2をSSL化」の「Apacheサーバ用のSSL鍵ファイルの準備」とほぼ同じです。

作業を"demoCA"のあるディレクトリで行なっていますが、これはUbuntuデフォルトのopenssl.cnfファイル(/etc/ssl/openssl.cnf)の中に、CA局のdirとして"./demoCA"が登録されていることに起因します。 これを変更すれば必ずしも"demoCA"のあるディレクトリで作業する必要はありません。

こう書いても常識的にはdemoCAディレクトリにcdしたくなるので、 まずは作業ディレクトリから$ ls -ld demoCAでdemoCAディレクトリがみえることを確認します。

$ ls -ld demoCA
drwxr-xr-x 6 yasu yasu 4096 2010-11-15 20:13 demoCA
実際の作業はここから

次に、まずは"newreq.pem"ファイルを作成します。 注意点は次の2点です。

  • 最初に入力する"PEM passphrase"は、後で作成する"newkey.pem(couchdb_stunnel_client_key.pem)"を使う時に必要になるので覚えておく
  • クライアント側のcertファイルに埋め込むCommon Nameは何でも良い (サーバはcacert.pemしかみないため)
/usr/lib/ssl/misc/CA.pl -newreq

いろいろ入力しますが、次のような内容になっています。 入力したところは強調表示にしています。

  PEM pass phrase:   ijsafuiowe890rwe
  ...
  Country Name (2 letter code) []:  JP
  State or Province Name (full name) []:  Fukushima
  Locality Name (eg, city) []:  AizuWakamatsu
  Organization Name (eg, company) []:  Yet Another Sundial Org.
  Organizational Unit Name (eg, section) []:  CouchDB Management Team
  Common Name (eg, YOUR name) []:  couchdb@example.org
  Email Address []:  admin@example.org
  
  Please enter the following 'extra' attributes
  to be sent with your certificate request
  A challenge password []:
  An optional company name []:
  Request is in newreq.pem, private key is in newkey.pem

続けて"newcert.pem"と"newkey.pem"を作成します。 ここでは"demoCA"のdemoCA/private/cakey.pemを開くために必要なパスフレーズを入力します。 あとは'y'を入力して作業を完了するだけです。

/usr/lib/ssl/misc/CA.pl -sign

カレントディレクトリにPEMファイルを放置しておくと、わけがわからなくなるのでディレクトリに移動しておきます。 一緒に作成した時に使ったdemoCAのcacert.pemもコピーしておきます。

$ mkdir demoCA.couchdb_stunnel_client_keys
$ cp demoCA/cacert.pem demoCA.couchdb_stunnel_client_keys/
$ mv newcert.pem newkey.pem demoCA.couchdb_stunnel_client_keys/
$ rm newreq.pem

デフォルトのnewcert.pemとnewkey.pemという名前も、配布する時には判りずらいので、それらしい名前にコピーしておきます。

$ cd demoCA.couchdb_stunnel_client_keys/
$ cp newcert.pem stunnel.client.cert.pem
$ cp newkey.pem stunnel.client.key.pem

さてnewreq.pemを作成した時のパスフレーズは覚えておかないと使えないので、あまりお勧めしませんが、passphraseなしにアクセスできる"stunnel.client.key.nopass.pem"ファイルを作成する事もできます。

openssl rsa < stunnel.client.key.pem > stunnel.client.key.nopass.pem

ここまでで次のファイルが作成できました。

  • cacert.pem
  • stunnel.client.cert.pem
  • stunnel.client.key..pem
  • stunnel.client.key.nopass.pem (これはなくてもいい)

作成したPEMファイルの配置

いよいよ作成したファイルを配置します。

stunnelの動いているサーバ側での作業

ここではstunnel.client.cert.pemと、cacert.pemファイルを配置します。c_rehashは必須です。

$ sudo cp stunnel.client.cert.pem cacert.pem /etc/couchdb/certs
$ sudo c_rehash /etc/couchdb/certs

クライアントのPEMファイルだけでは不十分で、そのPEMファイルが正規のものかを確認するために発行したCAのcacerts.pemファイルも必要なところがポイントです。

クライアント側でのファイル配置とスクリプトの変更

まずはRubyスクリプトのあるディレクトリに残りのPEMファイル(stunnel.client.*.pem)をコピーします。

まずはCouchDBモジュールのrequestメソッドを変更します。

書き換えたCouchモジュールのrequestメソッド v.2

    def request(req)
      client = Net::HTTP.new(@host, @port)
      if @options.kind_of?(Hash) and @options.has_key?('cacert')
        client.use_ssl = true
        client.ca_file = @options['cacert']
        client.verify_mode  = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
        client.verify_mode  = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
        client.verify_depth = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_depth')
        client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
        client.cert         = @options['ssl_client_cert'] if @options.has_key?('ssl_client_cert')
        client.key          = @options['ssl_client_key'] if @options.has_key?('ssl_client_key')
      end

      res = client.start { |http| http.request(req) }
      unless res.kind_of?(Net::HTTPSuccess)
        handle_error(req, res)
      end
      res
    end

次にRubyスクリプト本体を変更します。

変更したCouchモジュールを使うRubyスクリプトの抜粋 v.2


ssl_client_cert = OpenSSL::X509::Certificate.new(File.new('stunnel.client.cert.pem'))
ssl_client_key = OpenSSL::PKey::RSA.new(File.new('stunnel.client.key.pem'),'ijsafuiowe890rwe')
couch = Couch::Server.new("home.example.org","5984", {
  'cacert' => File.expand_path('cacert.pem', File.dirname($0)),
  'ssl_client_cert' => ssl_client_cert,
  'ssl_client_key'  => ssl_client_key
})

"stunnel.client.key.nopass.pem"を使う場合は、第2引数のパスフレーズは省略します。 これでStunnelを経由してCouchDBに接続する事ができるようになりました。

RubyのEOFError

cacert.pemファイルの読み込みがうまくいかないと、Rubyスクリプト内で例外を保続してprint $!をしてみてもなかなか不可解なエラーを発生します。

/usr/local/stow/ruby-1.9.2-p0/lib/ruby/1.9.1/net/protocol.rb:135:in `read_nonblock': Connection reset by peer (Errno::ECONNRESET)
	from /usr/local/stow/ruby-1.9.2-p0/lib/ruby/1.9.1/net/protocol.rb:135:in `rbuf_fill'
        ...

rbuf_fillとか、read_*メソッド関連のエラーは大抵はstunnelとかは関係なくて、クライアント側のコーディングとファイルの配置が間違っている可能性が高いです。

こういうエラーが出た場合には、curlやwgetでサーバが正しく動いているか確認するのがお勧めです。

$ cat stunnel.client.cert.pem stunnel.client.key.pem > stunnel.client.pem
$ curl -X GET -cacert cacert.pem --cert stunnel.client.pem:ijsafuiowe890rwe https://home.example.org:5984/example/

stunnelを起動する時に -D 7デバッグオプションをつけると、facilityがdaemonでsyslogに書き出されます。 Ubuntuであれば/var/log/daemon.logを確認すると解決策が分かるかもしれません。

まとめ

自分でもサーバ認証とクライアント認証はごっちゃになったり、わけもわからずに*cert.pemファイルを配置してc_rehashコマンドを打つ場合があります。

自分でアプリケーションを作れば知識も整理できるんでしょうけれど、stunnelをいきなり使おうとするとPEMファイルの作成が最初のハードルになります。

いろいろなスクリプトやガイドはありますが、opensslを実行するコマンドラインに -config ./openssl.cnfとか足されていると、それだけで、コマンドが実行できずに混乱するかもしれません。

楽をしようとスクリプトを使っても、クライアント認証まで持っていくには、そういう手間を省こうとした事で余計に混乱するかもしれません。

突き詰めればreq.pemを経由したcert.pemとkey.pemの2つのファイルしか扱わないんですけどね。cacert.pemが絡んで、意味もわからずコマンドを打っていくと、うまく動かないなんてことにもなるでしょう。

とりあえずは自分用のdemoCAを作成して繰り返し使うのがお勧めです。

企業内利用を想定したクライアント認証用PEMファイルを生成するスクリプトなどを公開している方はいて、中身を理解して使う分には効率を大きくあげてくれます。

CouchDBはちょうどモチベーションを保つのに良いセキュリティのなさ加減だったので、良い教材になりました。

2010/11/15

StunnelでCouchDBへの接続をSSLにしてみた

CouchDB Wiki - How to enable SSLにもあるように、CouchDB 1.1まではSSLが使えません。

もうちょっと待てば良いんですが、Basic認証が嫌だったので、stunnelのクライアント認証を使ってCouchDBへ接続する方法を試しました。

今回は図のようにCouchDBを127.0.0.1:5984で動かしたまま、クライアント認証を有効にしたStunnelを経由して外部システムからの接続を許可しています。

Webブラウザと外部システムがCouchDBに接続する様子

stunnelの起動に必要なPEMファイルの準備

stunnelを起動する際にはServe CertificateとしてのPEMファイルを準備する必要があります。 流れとしてはCA局の準備と、そのCA局を利用したcertとkey、2つのPEMファイルを作成する事になります。

今回は以前作成したCA局とApache2のSSL化で作成したPEMファイルをそのまま使います。 これはCertificateファイルのCommon Name欄が同じためです。

もし手元に他で作成したnewcert.pemファイルがあるとしても、Common Name欄がstunnelを動かすサーバと違うのであれば作り直す必要があります。

手元にある*cert.pemファイルのCommon Name(CN)欄の確認は次のコマンドで表示される文字列から"CN="に続く文字列を確認します。

$ openssl x509 -in newcert.pem -text | grep Subject:
        Subject: C=JP, ST=Fukushima, L=AizuWakamatsu, O=Yet Another Sundial Org., OU=Web Management Team,   CN=home.example.org
/emailAddress=admin@example.org

Common Name欄とサーバ名が一致しない場合や、IPアドレスでアクセスさせる場合には、そのIPアドレスをCommon Nameとして入力した"newcert.pem"(server_cert.pemにコピー)と"newkey.pem"ファイル(server_key.pemにコピー)が必要になります。

作業手順は、「 Ubuntu 8.04 LTS上のApache2をSSL化」の「CA局の準備」と「Apacheサーバ用のSSL鍵ファイルの準備」にあります。

「Apacheサーバ用のSSL鍵ファイルの準備」ではカレントディレクトリに"demoCA"ディレクトリがある事を前提に作業を始めている点に注意してください。

stunnel用PEMファイルの準備

ここから先は、前述の手順で作成した、 server_cert.pem (元はnewcert.pem)と server_key.pem (newkey.pem)の2つがあるところから作業を開始します。

いまは2つのPEMファイルが手元にありますが、stunnelで必要なPEMファイルの内容は次のような形になります。

...
-----BEGIN CERTIFICATE-----
MIIDRzCCArCgAwIBAgIJANXx3xE0zyt2MA0GCSqGSIb3DQEBBQUAMIGWMQswCQYD
...
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDb2b/AXAR+5l03swxxE7bktYmazVeWUcPPODXwvZWxSkTGjqVh
...
-----END RSA PRIVATE KEY-----

このPEMファイルの前半には、Certificateの内容をテキストで記述した内容がくっついていても構いません。

というわけで、このstunnel用のcertファイルを次の手順で作成します。

$ cat server_cert.pem server_key.pem > stunnel.pem
$ sudo cp stunnel.pem /etc/stunnel/stunnel.pem

/etc/stunnel/stunnel.pemはUbuntuに入っているstunnelのデフォルトのPEMファイルの格納先です。

さて、これでstunnelを起動する方の準備は整いました。

クライアント認証に向けた準備

とりあえずはクライアント用のCertificateを作成していないので、ディレクトリだけ準備しておきます。

この場所は /etc/couchdb/certs にしました。

手動で/usr/local以下にインストールしたCouchDBを使う場合には、/usr/local/etc/couchdb/certs とでもして、適宜読み替えてください。

Stunnelの起動

Stunnelを起動するのは簡単です。

$ stunnel -v 3 -a /etc/couchdb/certs -d 192.168.1.2:5984 -r 127.0.0.1:5984

問題はCouchDBと連動させてこれを動かす事ですが、CouchDBのリスタートとstunnelを連携させる必要はないので、とりあえず/etc/default/couchdbに埋め込む事にしました。

Ubuntu 10.04: 変更後の/etc/default/couchdbファイルの全体

# Sourced by init script for configuration.

COUCHDB_USER=couchdb
COUCHDB_STDOUT_FILE=/dev/null
COUCHDB_STDERR_FILE=/dev/null
COUCHDB_RESPAWN_TIMEOUT=5
COUCHDB_OPTIONS=

# Set this option to 0 if you don't want CouchDB server to start
# during system boot.
ENABLE_SERVER=1

## Added by Yasuhiro ABE
umask 027
/usr/bin/stunnel -v 3 -a /etc/couchdb/certs -d 192.168.1.2:5984 -r 127.0.0.1:5984

127.0.0.1:5984で動いているのはCouchDBプロセスだという点は、今回の作業のどこにも影響しないので、同じ手順で他のサービスにもSSL接続する事ができるようになります。

接続確認をするためにRuby + net/https を使ってクライアントを作成する事にします。

2010/11/14

CouchDBをソースコードからインストールしてみた

CouchDBを使いつつ、こんなんで大丈夫なのか、と思うところもあったので、試したことをまとめておきます。

O'ReillyのCouchDB - The Definitive Guideでは、やはりProduction環境ではいろいろ設定が必要だよ、という話しになっています。

セキュリティを低くしてとりあえず使える状態で配布しているのは普及させる戦略みたいですね。

今回使った環境は次のとおりです。 VMWare上で動かしたからといって特に違いはないはずです。

  • CouchDB 1.0.1
  • OS: Ubuntu 10.04 LTS 64bit版 on VMWare

インストール手順の落とし穴、足りないこと

ソースコードからインストールした場合の手順についてはCouchDB Wiki - Install_on_Ubuntuにあります。

囲みにある手順だけでは足りなくて、その下に書かている次のような点にも気をつける必要があります。

  • /etc/init.d/couchdbの配置(と、update-rc.dの実行)
  • /etc/logrotate.d/couchdbの配置
  • xulrunnerのバージョンアップへの対応

最初の2点はドキュメントの後半や、Example 1(Alternate)などでフォローされています。 また設定してしまえば特にフォローする必要はなさそうです。

このままだと自動で行なえないのは最後のxulrunnerのアップデートへの対応です。

また個人的に気になったのは次の点です。

  • 一般ユーザが(暗号化された)パスワードの書かれた設定ファイルにアクセスできる
  • 認証なしにDBにアクセスできる(別記事にまとめる)
  • Basic認証を送信する経路でデータが暗号化されない(別記事にまとめる)

ここでxulrunnerのバージョンアップと、ファイルのパーミッションについてまとめていきます。

xulrunnerのバージョンアップへのフォローアップ

/usr/local/lib/couchdb/bin/couchjs ファイルを適切に動かすためには libmozjs.so ライブラリファイルが必要です。

このlibmozjs.soファイルにアクセスするために、Ubuntu用のインストールガイドに従うと、xulrunner.confを作成することになります。 ガイドでは、この自ら作成したファイルがxulrunnerがアップデートされる際に自動で更新されない事を嘆いています。

この問題の本質は、ld.soがlibmozjs.soを見つけられない事にあるので、解決策は /etc/ld.so.conf.d以下を管理する方法の他に、LD_LIBRARY_PATHを適切に設定してあげる方法でも解決が可能です。

実際にUbuntu 10.04 LTSで普通に使えるcouchdbは0.10.0ですが、こちらでは起動スクリプト(/usr/bin/couchdb)の中でLD_LIBRARY_PATHを設定しています。

## grep LD_LIBRARY_PATH /usr/bin/couchdb on Ubuntu 10.04 LTS
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/xulrunner-`xulrunner-1.9.2 --gre-version`/
export LD_LIBRARY_PATH

Wikiでは /etc/ld.so.conf.d/xulrunner.conf を管理するように書かれていますが、これよりはパッケージと同様にLD_LIBRARY_PATHを設定する方法が楽しそうです。

最終的に/usr/local/etc/default/couchdbファイルを少し編集することにしました。

/usr/local/etc/default/couchdbファイル全体

# Sourced by init script for configuration.

COUCHDB_USER=couchdb
COUCHDB_STDOUT_FILE=/dev/null
COUCHDB_STDERR_FILE=/dev/null
COUCHDB_RESPAWN_TIMEOUT=5
COUCHDB_OPTIONS=

umask 027

XULDIR="/usr/lib/xulrunner-devel-$(xulrunner-1.9.2 --gre-version)"
if test -d "${XULDIR}" ; then
  LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${XULDIR}/lib"
  export LD_LIBRARY_PATH
else
  echo "[warn] Missing ${XULDIR} causes javascript exec error."
fi

[2011/01/21追記]
XULDIRの指定方法を間違えていたので、
・"devel"付きのディレクトリにアクセスし、
・LD_LIBRARY_PATHに${XULDIR}/libを指定するよう
修正しました。

libmozjs.soのバージョンが大きく変更されれば、再度コンパイルが必要でしょうから、これで解決として良いのかなと思います。

ファイルのパーミッション

次はファイルのパーミッションについてで、解決したいのは次のような点です。

  • adminsのID, Passwordがファイルに記述されるのに、一般ユーザから見える点
  • Wikiではディレクトリ単位で一般ユーザからのアクセスを拒否している記述は一部であるが不十分

全体でやりたい事は次のとおりです。

  • CouchDBはcouchdb:couchdb権限で実行する
  • 不必要にcouchdbユーザに書き込み権限を出さない
  • 一般ユーザが制御(API)を迂回してアクセスできないように、ID, Password情報は開示しない (/_users/org.couchdb.user:user_name文書を含む)

これだと基本root所有のファイルで、守るべき範囲はDBファイルと設定ファイルという事になります。

couchdbユーザが書き込みできなければいけない範囲は /usr/local/var 以下で、設定ファイルはIDの暗号化の時だけ必要ですが、これは手動で対応することで書き込み権限を落すことにしました。

ファイルのパーミッションを整える

参考にする導入手順は、Example1かExample 1 (Alternate)だと思いますが、そこに書かれているchmod, chownコマンドは無視をして、基本的に全ファイル root:root、drwxr-xr-x, -rw-r--r--(755 or 644)の権限だと仮定します。

まずはvarディレクトリ以下

$ sudo find /usr/local/var/lib/couchdb/. -exec chown couchdb:couchdb {} \;
$ sudo find /usr/local/var/lib/couchdb/. -exec chmod g-w,o-rwx {} \;
$ sudo find /usr/local/var/log/couchdb/. -exec chown couchdb:couchdb {} \;
$ sudo find /usr/local/var/log/couchdb/. -exec chmod g-w,o-rwx {} \;
$ sudo find /usr/local/var/run/couchdb/. -exec chown couchdb:couchdb {} \;

次にetcディレクトリ以下

$ sudo find /usr/local/etc/couchdb/. -exec chown root:couchdb {} \;
$ sudo find /usr/local/etc/couchdb/. -exec chmod g-w,o-rwx {} \;

注意点は、local.iniに書く[admins]の行は、以前の投稿のように、直接"-hashed-"で始まるハッシュに変換されたパスワードを書く必要があるところでしょうか。

不便に思う人もいるかもしれませんが、これで書き込み権限は /usr/local/var 以下に限定される事になります。 管理上は不要に構成が変更される危険を回避できるのでお勧めです。

新しく作成されるDBのパーミッション

正確には/usr/local/var以下のファイルが該当します。

前述の/usr/local/etc/default/couchdbの中に入れてありますが、/usr/local/etc/default/couchdbにumask 027を追加しました。

これで新しく作成されるDBは640(-rw-r-----)のパーミッションになります。

とりあえずは、この構成で使うことにしました。

番外編: xstowを使ったCouchDBの管理

普通はconfigureを実行する時にprefixを/usr/localとして導入すると思いますが、手元のマシンでは/usr/local以下に入れたものの管理のためにxstowコマンドを使っているため、prefixは/usr/local/stow/couchdb-1.0.1としています。

本来prefixを/usr/localとしてコンパイルしつつ、実際に導入する時には /usr/local/stow/couchdb-1.0.1に入れる、という使い方をするのですが、残念ながら全てのアプリケーションのインストールスクリプトがそういう方法に対応しているわけではありません。

それにxstowはバージョン管理をするために便利なのですが、DBのようなアプリケーションを使う時には少し工夫も必要です。

アプリケーションによっては、バージョンが変化しても変化して欲しくないものがいくつかあります。

  • 設定ファイル (couchdbでは local.ini と local.d/以下)
  • DBファイル (couchdbでは/usr/local/var/lib/couchdb/以下)
  • ログファイル等... (couchdbでは/usr/local/var以下のファイル全部)

こういうファイルはあらかじめ直接ファイルとして"/usr/local/var"以下などに配置してしまい、/usr/local/stow/couchdb-1.0.1/var以下からはファイルを削除しておくことで、xstowの管理対象から外してしまいます。

stow/couchdb-1.0.1以下から削除するファイル
  • /usr/local/stow/couchdb-1.0.1/varディレクトリ全体
  • /usr/local/stow/couchdb-1.0.1/etc/couchdb/local.iniファイル
  • /usr/local/stow/couchdb-1.0.1/etc/couchdb/local.dディレクトリ
  • /usr/local/stow/couchdb-1.0.1/etc/default/couchdbファイル

新しいcouchdbを/usr/local/stow以下にインストールした際には、xstowを実行する前に、これらに該当するファイル/usr/local/stow/couchdb-new-version/からをあらかじめ削除しておく必要があります。

リンクの削除とファイルの移動

一旦xstowを使って couchdb-1.0.1 関連のファイルを削除しておきます。

$ cd /usr/local/stow
$ sudo xstow -D couchdb-1.0.1

次に/usr/local/couchdb-1.0.1以下から、永続的に使用したいファイルを/usr/local/etcなどの/usr/local直下に移動します。

まずは/usr/local/var。

$ sudo mkdir -p /usr/local/var/{lib,run,log}
$ sudo mv /usr/local/stow/couchdb-1.0.1/var/lib/couchdb /usr/local/var/lib/
$ sudo mv /usr/local/stow/couchdb-1.0.1/var/log/couchdb /usr/local/var/log/
$ sudo mv /usr/local/stow/couchdb-1.0.1/var/run/couchdb /usr/local/var/run/
$ sudo rm -r /usr/local/stow/couchdb-1.0.1/var

次は/usr/local/etc/couchdb

$ sudo mkdir -p /usr/local/etc/couchdb/local.d
$ sudo mv /usr/local/stow/couchdb-1.0.1/etc/couchdb/local.ini /usr/local/etc/couchdb/
$ sudo rmdir /usr/local/stow/couchdb-1.0.1/etc/couchdb/local.d

最後は/usr/local/default。

$ sudo mkdir -p /usr/local/etc/default/
$ sudo mv /usr/local/stow/couchdb-1.0.1/etc/default/couchdb /usr/local/etc/default/
$ sudo rmdir /usr/local/stow/couchdb-1.0.1/etc/default
xstowの際実行

作業が終ったら、xstowを使ってcouchdb-1.0.1を/usr/local直下に配置します。

$ cd /usr/local/stow
$ sudo xstow couchdb-1.0.1

あとは必要な設定はdefault.iniではなく、local.iniを使うように徹底するところと、次回インストールした最新版のcouchdb-1.1などは、/usr/local/stow に置いた後に、var, etc/couchdbなどを削除する事が必要になります。

設定ファイルから/usr/local/stowで始まるパスを削除する

今回はprefixを/usr/localにして、make installの際にprefixを/usr/local/stow/couchdb-1.0.1にする方法を取らなかったので、手動で設定ファイルから /usr/local/stow/couchdb-1.0.1 で始まる文字列を /usr/loocal に置換する必要がありました。

対象のファイルは次の通りでした。

  • /usr/local/stow/couchdb-1.0.1/bin/couchdb
  • /usr/local/stow/couchdb-1.0.1/bin/couchjs
  • /usr/local/stow/couchdb-1.0.1/etc/init.d/couchdb
  • /usr/local/stow/couchdb-1.0.1/etc/logrotate.d/couchdb

次回は認証についての方法をまとめていきます。