2011年12月31日土曜日

worker MPM じゃないと ttl が動かない

サイオス鎌田です。
2011年ももうすぐ終わりですね。
今回は、Apache のプロキシ設定に関するお話です。

Apache のプロキシ設定に、コネクションの生存時間を設定する ttl 設定があることはご存じでしょうか。

ProxyPass ディレクティブ
http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypass

実はこの ttl 設定、Apache のコンパイル時にデフォルトで選択されたり、RHEL 等のデフォルトである prefork MPM では動作しません。ソースコード上ではスレッド MPM が有効な場合のみ動作するよう設計されているんです。従って、worker MPM 等のスレッド MPM でなければ ttl が使えません。

そのため、例えば prefork MPM のまま ttl 設定を行い、プロキシ先のサーバ、例えば Tomcat で connectionTimeout を設定していると、Apache からコネクションを切断することがないため、Tomcat からコネクションの切断要求が送信されたままになってしまい、CLOSE_WAIT の状態になってしまいます。

現在の Apache の trunk 上にあるソースコードを見た限りでは、スレッド MPM 限定となるような設計にはなっていなかったので、おそらく Apache 2.4 からは prefork MPM でも ttl が動作すると思われますが、Apache 2.0 系または Apache 2.2 系で ttl を使う場合は、worker MPM を使う方がいいかもしれません。

2011年12月28日水曜日

Spam対策 -Postfix-

こんにちは、サイオステクノロジー 稲垣です。

前回に引き続き、Postfix の設定についてご紹介します。
今回は、Postfix での Spam 対策についてです。

Postfix で利用可能な Spam 対策を、以下に示します。

2011年12月27日火曜日

sendmail - ヘルプでのバージョン情報の隠蔽

サイオス 小川です。デフォルトの状態で telnet で sendmail にアクセスして、ヘルプを表示させると下記のように表示されます。

220 [FQDN] ESMTP Sendmail 8.13.7/8.13.7; Fri, 22 Oct 2010 12:04:15 +0900 (JST)
help
214-2.0.0 This is sendmail version 8.13.7
214-2.0.0 Topics:
214-2.0.0 HELO EHLO MAIL RCPT DATA
214-2.0.0 RSET NOOP QUIT HELP VRFY
214-2.0.0 EXPN VERB ETRN DSN AUTH
214-2.0.0 STARTTLS
214-2.0.0 For more info use "HELP ".
214-2.0.0 To report bugs in the implementation see
214-2.0.0 http://www.sendmail.org/email-addresses.html
214-2.0.0 For local information send email to Postmaster at your site.
214 2.0.0 End of HELP info

2011年12月26日月曜日

sendmail - メールヘッダのバージョン情報の隠蔽

サイオス 小川です。sendmail をデフォルト状態で使用するとメールヘッダに sendmail のバージョン情報が出力されます。下記 (8.13.8/8.13.8) の部分が該当します。

sendmail のバージョン情報を削除する方法

Received: from smtp (rhel5-5 [127.0.0.1])
by rhel5-5.labs.sios.com (8.13.8/8.13.8) with ESMTP id o913QBHg002003
for root@example.com; Fri, 1 Oct 2010 12:27:01 +0900

2011年12月22日木曜日

CentOS 6.2 リリースされました

サイオステクノロジー 金田です。

12/9 に CentOS 6.1 が出たばかりですが、12/20 には CentOS 6.2 が早々とリリースされたので、早速 CentOS 6.2 をインストールしてみました。

インストール手順などは CentOS 6.0 と特に変わっているところはありませんでしたが、個人的に気になっていた点として、CentOS 6.0 では「システム」→「設定」→「ウィンドウ」の項目が無かった(画像1)ので、CentOS 6.1/6.2 で復活するかどうか注目していました。

画像1 CentOS 6.0 「システム」→「設定」メニュー

結果的に CentOS 6.1 で復活したようで(※)、CentOS 6.2 でも使用できるようになっています。(画像2)
※:Scientific Linux 6.1 や RHEL 6.1 で利用可能であることはすでに確認していました。

画像2 CentOS 6.2 「システム」→「設定」メニュー

sendmail - グリーティング・メッセージでのバージョン情報の隠蔽

サイオス 小川です。デフォルトの状態で telnet で sendmail にアクセスすると下記のように出力されます。

220 [FQDN] ESMTP Sendmail 8.13.7/8.13.7; Fri, 22 Oct 2010 12:04:15 +0900 (JST)

これに関するデフォルトの /etc/mail/sendmail.cf 設定は下記の通りです。

SmtpGreetingMessage=$j Sendmail $v/$Z; $b

この設定を sendmail.mc を使って変更します。次の行を sendmail.mc に追加します。

define(`confSMTP_LOGIN_MSG', `$j unknown; $b')dnl

sendmail.cf を作成し直して、sendmail を再起動します。この時の sendmail.cf は下記の通りです。

SmtpGreetingMessage=$j unknown; $b

telnet で sendmail にアクセスすると下記のようになります。

220 [FQDN] ESMTP unknown; Fri, 22 Oct 2010 14:26:33 +0900 (JST)

2011年12月21日水曜日

sendmail - mailertable による冗長化

サイオス 小川です。

/etc/mail/mailertable に下記のように記述することにより、経路の冗長化をすることができます。

フォールバック

domain smtp:[IPアドレス1]:[IPアドレス]:[IPアドレス]

ラウンドロビン

domain smtp:[IPアドレス1],[IPアドレス],[IPアドレス]

sendmail で宛先を限定する方法

サイオス 小川です。mailertable を使用して宛先を制限します。

1. /etc/mail/mailertable で下記の様に localhost と送信を許可するドメインの設定をします。また、最後の行で指定のない宛先に対してはエラー"error:5.7.1:553 Relaying denied" を返却するように設定します。

/etc/mail/mailertable

localhost local:
example1.com smtp:example1.com
example2.com smtp:example2.com
example3.com smtp:example3.com
. error:5.7.1:553 Relaying denied

2. make mailertable.db を実行し、mailertable.db を更新する。

この設定により下記の動作をするようになります。

  • <ユーザー名@localhost> 宛のメールはローカルスプールに受信します。
  • <ユーザー名@example1.com> 宛等のメールは DNS を参照して転送先が決められます。
  • それ以外のメールはエラーで転送拒否されます。

2011年12月20日火曜日

DNS による名前解決を行わない設定 -Postfix-

サイオステクノロジー 稲垣です。

Postfix を使用して、受信したメールを DNS による名前解決を使わずに main.cf の relayhost の設定を使って送信する方法をご紹介します。

relayhost の転送先ホスト名または IP アドレスを、角括弧 ("[" "]") で囲みます。このように設定すると、Postfix は該当ホスト名または IP アドレスについて、DNS による名前解決を行わず (DNS クエリーを発行せず) に処理を行います。

設定例

relayhost = [192.168.xx.xx]

この際、/etc/hosts に該当ホスト名が設定されていなければエラーになります。

あるいは、main.cf にて下記行を追加することで、Postfix は全てのホスト名または IP アドレスについて、DNS による名前解決をせずに処理を行います。

disable_dns_lookups = yes

2011年12月16日金曜日

bind 脆弱性 CVE-2011-4313 リンクまとめ

サイオステクノロジー 金田です。

DNS サーバ bind の脆弱性 CVE-2011-4313 についてリンクをまとめてみました。

bind の開発元である ISC の英語ページは 12/5 付で更新されていますが、日本語のページは更新されていません。日本語の情報としては IIJ-SECT の情報がわかりやすいと思います。

CentOS 6 系で NIC 交換すると eth0 と認識されない

サイオステクノロジー 金田です。

CentOS6 (RHEL6/Scientific Linux6) では、HW 不良等により NIC を交換すると、交換したNICが eth0 ではなく eth1 と認識されてしまいます。

MAC アドレスが異なる NIC が追加されるたびに /etc/udev/rules.d/70-persistenet-net.rules にエントリが追加されてしまうためにこのような現象になるのですが、交換した NIC を eth0 と認識させる方法について以下に紹介します。

(1) MAC アドレスの確認
/etc/sysconfig/network-scripts/ifcfg-eth1 がない場合、ifconfig eth1 を実行し HWaddr の内容を記録しておきます。

(2) ネットワーク関連サービスの停止

# service NetworkManager stop (*)
# service network stop
*:NetworkManager がインストールされている場合

(3) ネットワークモジュールのアンロード

# rmmod <ドライバ名>
※ドライバ名は /etc/udev/rules.d/70-persistent-net.rules ファイル内の NIC 設定行の1行上のコメント行の末尾に () で括られて記載されています。
  (下記例は vmnet3)
# PCI device 0x15ad:0x07b0 (vmxnet3)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:0c:29:79:22:65", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"

(4) ifcfg-ethX ファイルのリネームまたは修正
/etc/sysconfig/network-scripts/ifcfg-eth1 がある場合は、ifcfg-eth0 ファイルと ifcfg-eth1 ファイルを次のように変更します。

# mv ifcfg-eth0 ifcfg-eth0.bak
# mv ifcfg-eth1 ifcfg-eth0
/etc/sysconfig/network-scripts/ifcfg-eth1 がない場合は、ifcfg-eth0 の HWADDR= の値を (1) で保存した値に書き換えます。

(5) /etc/udev/rules.d/70-persistent-net.rules のバックアップ

# mv /etc/udev/rules.d/70-persistent-net.rules \
     /etc/udev/rules.d/70-persistent-net.rules.bak

(6) ネットワークモジュールのロード

# modprobe <ドライバ名>

(7) ネットワーク関連サービスの起動

# service network start
# service NetworkManager start (*)
*:NetworkManager がインストールされている場合


上記は、eth1 として認識してしまった後の変更方法ですが、/etc/udev/rules.d/70-persistent-net.rules に NIC のエントリを作成されないようにする方法も併せて紹介します。

/lib/udev/rules.d/75-persistent-net-generator.rules にある下記の行をコメントアウトすることで、/etc/udev/rules.d/70-persistenet-net.rules に新しい NIC の情報は追加されなくなります。

(変更前)

DRIVERS=="?*", IMPORT{program}="write_net_rules"
(変更後)
# DRIVERS=="?*", IMPORT{program}="write_net_rules"

Trouble Maker を触ってみました

サイオス 鎌田です。

数日前に、Gigazine のニュースに Trouble Maker というツールが紹介されていました。これは、Linux サーバに細工を施し、システムが起動しなくなる等のトラブルをわざと引き起こすツールで、Linux のトラブル解決の練習になるようなものです。というわけで、一つ検証環境を用意して、さっそく動かしてみました。

Trouble Maker
http://trouble-maker.sourceforge.net/

まずはインストール方法から。難しいことはありません。

# rpm -Uvh http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el5.rf.i386.rpm
# yum install perl-YAML
# wget http://sourceforge.net/projects/trouble-maker/files/trouble-maker/0.11/trouble-maker-0.11.tgz/download
# cd /
# tar xzvf /path/to/trouble-maker-0.11.tgz

起動してみます。

# /usr/local/trouble-maker/bin/trouble-maker.pl --version=RHEL_5

Trouble-maker version 0.10
Copyright (C) 2004 by Josh More
Trouble-maker comes with ABSOLUTELY NO WARRANTY
This is free software, and you are welcome to redistribute it under certain conditions.
For details, see the file 'COPYING' included with this distribution.

This program is intended for training purposes only, and will cause system problems.
To proceed, type 'yes'.
(To bypass this step, change the value of the $bypass variable to '1')
>yes
PROBLEM:
The system will not boot

さらっと、システムが起動しなくなると怖いことを言ってきます。実際にここで再起動すると、次のメッセージが出て OS が起動しなくなります。ぜひ、皆さんも検証用の環境を作って動かしてみてはいかがでしょうか。

Kernel panic - not syncing: Attempted to kill init!

2011年12月15日木曜日

CVE-2011-3192 の新しい攻撃コードが公開されています

お久しぶりです。サイオス 鎌田です。今年も残すところあとわずかとなりました。今回は脆弱性のお話です。

夏の終わりごろ、HTTP サーバとして有名な Apache HTTPD に、CVE-2011-3192 という脆弱性が存在することが発覚し、アナウンスから 48 時間以内に修正を実施することを宣言するなど、少し大きな騒ぎとなりました。

現在は修正版もリリースされていますし、バージョンアップを行わずに設定を変更することで回避している方もいらっしゃるかと思います。設定を変更することで回避する方法として、当初 Apache HTTPD のアナウンスでは、Range ヘッダに 5 つ以上範囲指定されているリクエストを無視したり、HTTP リクエストのヘッダサイズを制限したり、Range ヘッダの指定をそもそも無視するといった方法を紹介していました。

今回、公開された新しい攻撃コードは、この設定変更による回避策のうち、Range ヘッダに 5 つ以上範囲指定されているリクエストを無視する方法と、HTTP リクエストのヘッダサイズを制限する方法を破る攻撃となっています。具体的には、Range ヘッダには 2 つの範囲指定を行い、Accept-Encoding ヘッダに gzip を指定したリクエストを頻繁に行うという、単純、かつ効果的に DoS を実現させる攻撃となっています。

そのため、Range ヘッダを無視することで回避を行っている方には影響がないのですが、どうしても Range ヘッダを有効化する方にとっては非常に厄介な攻撃コードが公開されたことになります。こうなると、対策方法としては Apache HTTPD をバージョンアップするしかありません。

このように、当初は良かった対策でも、新しい攻撃コードが公開されることによって再度脅威にさらされるというケースがありますので、一度稼働を始めたバージョンを使い続けるだけでなく、定期的にバージョンアップを行うことを前提とした運用を検討されることをお勧めいたします。

なお、現在公開されている最新の Apache HTTPD のバージョンは、2.2.21 となっています。

PostgreSQL で、STATISTICS (列の統計精度) を変更した箇所を忘れたら

お世話になっております、サイオス 那賀です。

PostgreSQL では、特定のテーブルの特定のカラムごとに、統計情報を取得する際の精度を指定することで、大きなテーブルでは荒くなりがちな統計情報の精度を調整することができるようになっています。

postgres=# ALTER TABLE users ALTER COLUMN name SET STATISTICS 500;
ALTER TABLE

しかし、いろいろやっていると、データベース全体のどこに指定をしたか忘れてしまいます。そんな時は、以下のようにすれば変更箇所を一覧することができます。

postgres=# SELECT c.relname, a.attname, a.attstattarget
  FROM pg_attribute AS a, pg_class AS c
  WHERE c.oid = a.attrelid and a.attstattarget > 0;
 relname | attname | attstattarget
---------+---------+---------------
 users   | name    |           500
(1 row)

ところでこの値のデフォルトである "default_statistics_target" パラメータは、VACUUM の効率やマシン性能の向上に合わせて、徐々に上がっています。

18.6 問い合わせ計画 - PostgreSQL 8.3 文書

default_statistics_target(integer)…デフォルトは10です。

18.6 問い合わせ計画 - PostgreSQL 8.4 文書

default_statistics_target(integer)…デフォルトは100です。

以前は、10 では小さすぎるので調整をすると性能が上がりましたが、100 もあれば、よほど大きなテーブルでなければ問題なくなった模様。

では。

2011年12月14日水曜日

ソースの入手と再ビルド ~ Amazon Linux AMI (EC2) 編

お疲れ様です、サイオス 那賀です。

Amazon Linux AMI は、AWS (Amazon Web Services) 専用に、Amazon.com によって開発されている Linux ディストリビューションの仮想環境イメージ (AMI: Amazon Machine Image) です。今のところ実態としては、RHEL6 系に近いものになっています。てっきり CentOS あたりをベースにリコンパイルだけをして作っているのかと思っていたのですが、RHEL6 を下敷きにしつつも、自前で全部作っているそうです。AWS の機能に特化されているのみならず、Amazon のサポートに通りが良いところがウリです。

そのようなわけですから、Amazon Linux もまた RPM ベースです。Amazon Linux も YUM は備えているのですが、ソースの入手には yumdownloader ではなく、別途 get_reference_source コマンド経由で、web サービスを利用します。右記参照、「Q: Amazon Linux AMI へのソースコードを見ることができますか? - Amazon Elastic Compute Cloud (Amazon EC2) FAQ」。

$ get_reference_source --aws-account-id=XXXX-XXXX-XXXX \
 --package=which

Requested package: which

Found package from local RPM database: which-2.19-5.1.9.amzn1
Corresponding source RPM to found package : which-2.19-5.1.9.amzn1.src.rpm

Your AWS account id: XXXX-XXXX-XXXX

Are these parameters correct? Please type 'yes' to continue: yes
Source RPM for 'which-2.19-5.1.9.amzn1' downloaded to: /usr/src/srpm/debug/which-2.19-5.1.9.amzn1.src.rpm
[ knaka@ip-10-146-81-102:~ S ]
$ ls -ld /usr/src/srpm/debug
drwxrwxrwt 2 root root 4096 Dec XX 03:36 /usr/src/srpm/debug/

それ以降は他の distro と大差ないのですが、以前の記事(「SIOS OSS Tech: debuginfo RPM パッケージで、ソースレベル・デバッグをする」)などで触れた Red Hat 系における "redhat-rpm-config" パッケージが、Amazon Linux では "system-rpm-config" パッケージにあたるので、既存パッケージと同等の構成でインストールしたい場合や、特に、debuginfo パッケージが必要な場合には、これもインストールしておいてください。

$ sudo yum install -y system-rpm-config
$ rpmbuild --rebuild --define="optflags -pg -g3 -O0" \
 /usr/src/srpm/debug/which-2.19-5.1.9.amzn1.src.rpm
(中略)
Wrote: /home/knaka/rpm/RPMS/i686/which-2.19-5.1.9.amzn1.i686.rpm
Wrote: /home/knaka/rpm/RPMS/i686/which-debuginfo-2.19-5.1.9.amzn1.i686.rpm
(中略)

では。

2011年12月13日火曜日

PostgreSQL の SQL やプロシージャでランダムデータの入ったテーブルを生成する

お疲れ様です、サイオス 那賀です。

テーブルの抱えるサンプルデータの行数が 2~3 行だと、何をどうしてもインデックスなど使われないのでつまらない時に、自動的に生成した沢山のランダムデータを INSERT する方法です。連番 10000 個につき、ランダムな 20 文字からなる名前をそれぞれに持つ "users" テーブルを作成してみます。

SQL のみで実行

まずは、バージョン 9.0 以降での例です。

DROP TABLE users CASCADE;

CREATE TABLE users (id integer PRIMARY KEY, name text);

INSERT INTO users (id, name) (
  SELECT id,
   string_agg(substr(s, ceil(random() * length(s))::int, 1),'')
    FROM (
      SELECT 'abcdefghijklmnopqrstuvwxyz'
       'ABCDEFGHIJKLMNOPQRSTUVWXYZ'::text s, *
        FROM generate_series(1, 10000) AS id, generate_series(1, 20)
    ) AS tmp
    GROUP BY id
);

CREATE INDEX ON users (name); -- 9.0 から、インデックス名は省略可能

ここでは、新しく導入された string_agg() 集約関数を使用しているため、8.4 以前では動きません。サブクエリをそれぞれ実行してみれば、何をしているかは分かると思います。

ランダムな文字列が生成されていることと、充分にデータが大きいのでインデックスが使われていることを確認します。

postgres=# SELECT * FROM users LIMIT 10;
 id |         name
----+----------------------
  1 | bCphDuvZliwsKjSciOZA
  2 | oYTQPutKQqYRTnYwIuwU
  3 | DSmnbfqkTpKioEZeZsOP
  4 | JNHCbFYJauDDnPRpVizO
  5 | xkXMPWQOoFEXslAtRycS
  6 | TGvhwnxrvWgThdFXawMo
  7 | bqmtCNNulQmfwInTWKkr
  8 | HrlPvRMvnyJpPWJrJwLV
  9 | mYbJGoCCZNUGefvzXiUl
 10 | GEAwBkNlGzgTxiCexfGw
(10 rows)

postgres=# EXPLAIN SELECT * FROM users WHERE name = 'hoge';
                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on users  (cost=4.64..75.57 rows=50 width=36)
   Recheck Cond: (name = 'hoge'::text)
   ->  Bitmap Index Scan on users_name_idx  (cost=0.00..4.63 rows=50 width=0)
         Index Cond: (name = 'hoge'::text)
(4 rows)

postgres=# 

バージョン 8.4 でも、配列に対する集約関数である array_agg() 関数が使えるので、以下のようにできます。

DROP TABLE users CASCADE;

CREATE TABLE users (id integer PRIMARY KEY, name text);

INSERT INTO users (id, name) (
  SELECT id, array_to_string(
   array_agg(a[ceil(random() * array_length(a, 1))]),'' )
    FROM (
      SELECT ARRAY[
       'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
       'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
       'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
       'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
      ] AS a, *
        FROM generate_series(1, 10000) AS id, generate_series(1, 20)
    ) AS tmp
    GROUP BY id
);

CREATE INDEX users_name_idx ON users (name);

8.3 以前では、array_agg() も無いので、集約関数では無理だと思います。ストアドプロシージャを使います。

ストアドプロシージャで実行

9.0 以降には DO コマンドがあり、無名関数の実行ができますので "DO $$~" のように実行できますが、8.4 以前では、一旦名前をつけて関数を定義する必要があります。

以下は、8.3 でも動きます。

DROP TABLE users CASCADE;

CREATE TABLE users (id integer PRIMARY KEY, name text);

CREATE OR REPLACE FUNCTION tmp() RETURNS void AS
$$
  DECLARE
    i INTEGER;
    id INTEGER;
    name text;
    s text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  BEGIN
    FOR id IN 1 .. 10000 LOOP
      name := '';
      FOR i IN 1 .. 20 LOOP
        name := name || substr(s, ceil(random() * length(s))::int, 1);
      END LOOP;
      INSERT INTO users VALUES(id, name);
    END LOOP;
  END
$$ LANGUAGE plpgsql;

SELECT tmp();

DROP FUNCTION tmp();

CREATE INDEX users_name_idx ON users (name);

では。

2011年12月12日月曜日

RHEL6.1~系の OpenLDAP (slapd.conf) で SSH 公開鍵配布

お世話になっております、SIOS 那賀です。

先日掲載した「SIOS OSS Tech: RHEL6 系での OpenLDAP サーバと PAM 認証」で、slapd.conf 型から cn=config 型への移行を頑張ろうと決意したのですが、「Convert schema files for import - OpenLdap: Switch to dynamic config backend (cn=config) - Zarafa wiki」を読んでアホらしくなってしまい、slapd.conf へ戻すことにしました。今回の主題は OpenLDAP を使った SSH キーの配布なのですが、その前段として、前回の手順一通りを、slapd.conf を用いるものに書き換えて再掲しようと思います。cn=config 方式にも有用な場面は(多分)あるかと思われますので、既存の記事も、そのまま残しておきます。

ディレクトリ・サーバの設定

まずはいつものように、SELinux と iptables の無効化です。実運用のサーバでは、運用ポリシーに従って、それぞれ適切に設定してください。

[root@dirserv ~]# setenforce permissive
[root@dirserv ~]# getenforce
Permissive
[root@dirserv ~]# service iptables stop
iptables: ファイアウォールルールを消去中:                  [  OK  ]
iptables: チェインをポリシー ACCEPT へ設定中filter         [  OK  ]
iptables: モジュールを取り外し中:                          [  OK  ]
[root@dirserv ~]#

サーバと、操作用のクライアントをインストールします。

[root@dirserv ~]# yum install -y openldap-clients openldap-servers

もし動いているようであれば、slapd サービスを停止させてから、slapd.d/ ディレクトリを削除します。これで、slapd は slapd.conf から設定を読むようになります。

[root@dirserv ~]# service slapd status
slapd は停止しています
[root@dirserv ~]# rm -fr /etc/openldap/slapd.d/ /var/lib/ldap/*

"Obsolete" のファイルを元に、slapd.conf を作成します。

[root@dirserv ~]# slappasswd -s secret2
{SSHA}Z42Qdb/r/YAw0QRD8vI/SYcLgoxtzF4l
[root@dirserv ~]# cp \
 /usr/share/openldap-servers/slapd.conf.obsolete \
 /etc/openldap/slapd.conf
[root@dirserv ~]# chown ldap.ldap /etc/openldap/slapd.conf
[root@dirserv ~]# chmod 0600 /etc/openldap/slapd.conf
[root@dirserv ~]# vi /etc/openldap/slapd.conf
[root@dirserv ~]# diff -uNr \
 /usr/share/openldap-servers/slapd.conf.obsolete \
 /etc/openldap/slapd.conf
--- /usr/share/openldap-servers/slapd.conf.obsolete
+++ /etc/openldap/slapd.conf
@@ -64,9 +64,9 @@
 # /etc/pki/tls/certs, running "make slapd.pem", and fixing permissions on
 # slapd.pem so that the ldap user or group can read it.  Your client software
 # may balk at self-signed certificates, however.
-# TLSCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
-# TLSCertificateFile /etc/pki/tls/certs/slapd.pem
-# TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem
+TLSCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
+TLSCertificateFile /etc/pki/tls/certs/slapd.pem
+TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem

 # Sample security restrictions
 #      Require integrity protection (prevent hijacking)
@@ -95,14 +95,26 @@
 #
 # rootdn can always read and write EVERYTHING!

+access to attrs=userPassword
+  by dn="cn=Manager,dc=example,dc=com" write
+  by self write
+  by anonymous auth
+  by * none
+
+access to *
+  by dn.exact="cn=Manager,dc=example,dc=com" write
+  by self write
+  by * read
+
 #######################################################################
 # ldbm and/or bdb database definitions
 #######################################################################

 database       bdb
-suffix         "dc=my-domain,dc=com"
+suffix         "dc=example,dc=com"
 checkpoint     1024 15
-rootdn         "cn=Manager,dc=my-domain,dc=com"
+rootdn         "cn=Manager,dc=example,dc=com"
+rootpw         {SSHA}Z42Qdb/r/YAw0QRD8vI/SYcLgoxtzF4l
 # Cleartext passwords, especially for the rootdn, should
 # be avoided.  See slappasswd(8) and slapd.conf(5) for details.
 # Use of strong authentication encouraged.

起動します。今回はテストですので、インデックスも BDB の設定も、そのまま放置します。実運用では、パフォーマンスやバックアップにも気をつかってあげてください。

[root@dirserv ~]# service slapd start
slapd を起動中:                                            [  OK  ]

以下は任意ですが、サーバの挙動が分かりやすいように、ログの出力も設定しておきます。slapd は local4 のファシリティで syslog 出力をするのですが、せっかくの RHEL6 系の rsyslog ですので、アプリケーション名 ("slapd") でログを振り分けるようにしてみます。実運用では、ログが溢れないように、ローテートの設定もしておいてください。

[root@dirserv ~]# cp /etc/rsyslog.conf /etc/rsyslog.conf.orig
[root@dirserv ~]# vi /etc/rsyslog.conf
[root@dirserv ~]# diff -uNr /etc/rsyslog.conf.orig /etc/rsyslog.conf
--- /etc/rsyslog.conf.orig
+++ /etc/rsyslog.conf
@@ -76,3 +76,6 @@
 # remote host is: name/ip:port, e.g. 192.168.0.1:514, port optional
 #*.* @@remote-host:514
 # ### end of the forwarding rule ###
+
+!slapd
+*.* /var/log/slapd.log
[root@dirserv ~]# service rsyslog reload
Reloading system logger...                                 [  OK  ]

アクセスできるかどうか、試してみます。

[root@dirserv ~]# ldapsearch -x -D "cn=Manager,dc=example,dc=com" \
 -b "dc=example,dc=com" -w secret2
# extended LDIF
#
# LDAPv3
# base  with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

良いようであれば、認証用のデータを入れて行きます。根元から作って行きます。

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: dc=example,dc=com
dc: example
objectClass: dcObject
objectClass: organization
o: Example Corp.

dn: cn=Manager,dc=example,dc=com
cn: Manager
objectClass: organizationalRole

dn: o=Users,dc=example,dc=com
o: Linux Users
objectClass: organization

dn: ou=People,o=Users,dc=example,dc=com
ou: People
objectClass: organizationalUnit

dn: ou=Group,o=Users,dc=example,dc=com
ou: Group
objectClass: organizationalUnit
EOF
adding new entry "dc=example,dc=com"

adding new entry "cn=Manager,dc=example,dc=com"

adding new entry "o=Users,dc=example,dc=com"

adding new entry "ou=People,o=Users,dc=example,dc=com"

adding new entry "ou=Group,o=Users,dc=example,dc=com"

ちょっと間違うと、途中まで作ったところでエラーになり、そこまでの分が半端に残ります。failure atomic ではありません…、トランザクションがあればいいのにね。今回はテストですので、そんな時は、以下のように根元からガバッと再帰で消してしまってください。

[root@dirserv ~]# ldapdelete -v -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 -r "dc=example,dc=com"

ユーザ (foo と bar) を追加します。OpenLDAP のクライアント側 (SSSD, pam_ldap, nss-pam-ldapd 等) は、文書にはあまりキチンと書かれていないのですが (せいぜい /usr/share/doc/nss-pam-ldapd-0.7.5/README くらいか)、デフォルトで、クラス "posixAccount" や "posixShadow" を持つノードを、ユーザ情報として取りにきます。ですので、実はツリー構造は割と恣意的に決めてしまっても問題ありません。

[root@dirserv ~]# pass=$(slappasswd -s secret3)
[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: uid=foo,ou=People,o=Users,dc=example,dc=com
objectClass: top
objectClass: posixAccount
objectClass: account
gecos: ldap system users
cn: foo
uid: foo
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/foo
loginShell: /bin/bash
userPassword: $pass
EOF
adding new entry "uid=foo,ou=People,o=Users,dc=example,dc=com"

[root@dirserv ~]# pass=$(slappasswd -s secret4)
[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: uid=bar,ou=People,o=Users,dc=example,dc=com
objectClass: top
objectClass: posixAccount
objectClass: account
gecos: ldap system users
cn: bar
uid: bar
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/bar
loginShell: /bin/bash
userPassword: $pass
EOF
adding new entry "uid=bar,ou=People,o=Users,dc=example,dc=com"

グループ (foo と bar) を追加します。こちらも同様に、クライアントはデフォルトで、クラス "posixGroup" を持つノードを探索に来ます。今回は、昨今の Linux ディストリビューションの構成と同様に、UPG (User Private Group) 式で、ユーザごとに個別に同名のグループを割り当てますが、LDAP 的には、共通のグループを割り当てる方が一般的かも知れません。

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: cn=foo,ou=Group,o=Users,dc=example,dc=com
cn: foo
objectClass: posixGroup
gidNumber: 1000
userPassword: {CRYPT}x
EOF
adding new entry "cn=foo,ou=Group,o=Users,dc=example,dc=com"

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: cn=bar,ou=Group,o=Users,dc=example,dc=com
cn: bar
objectClass: posixGroup
gidNumber: 1001
userPassword: {CRYPT}x
EOF
adding new entry "cn=bar,ou=Group,o=Users,dc=example,dc=com"

一般ユーザから、他人のパスワードが見えないことを確認しておきます。

[root@dirserv ~]# ldapsearch -x \
 -D "uid=foo,ou=People,o=Users,dc=example,dc=com" \
 -b "dc=example,dc=com" -w secret3 "(objectClass=posixAccount)"
[root@dirserv ~]# ldapsearch -x \
 -D "uid=bar,ou=People,o=Users,dc=example,dc=com" \
 -b "dc=example,dc=com" -w secret4 "(objectClass=posixAccount)"

認証依頼元の Linux ホストの設定

こちらも同様に、SELinux と iptables の無効化です。実運用のサー (以下テンプレ略)。

[root@linhost ~]# setenforce permissive
[root@linhost ~]# getenforce
Permissive
[root@linhost ~]# service iptables stop
iptables: ファイアウォールルールを消去中:                  [  OK  ]
iptables: チェインをポリシー ACCEPT へ設定中filter         [  OK  ]
iptables: モジュールを取り外し中:                          [  OK  ]
[root@linhost ~]#

RHEL5 系の nss_ldap から、RHEL6 系では PAM は pam_ldap に、NSS は nss_ldap からフォークした nss-pam-ldapd へと分離しており、また、それらを束ねる SSSD 抽象化レイヤ (詳細) が入っていたりと、設定ファイルが多岐に渡り、もはや手作業では手に負えなくなっているので、authconfig コマンドを使用します。

同時に、TLS (389 番ポートでの "STARTTLS" による接続) も設定します。authconfig では、認証局ナシでの TLS 設定ができないので、設定をした後で、ちょっとだけ手を入れる必要があります。

SSSD を用いる手順:

[root@linhost ~]# authconfig --disableforcelegacy --enablemkhomedir \
 --enableldap --enableldapauth --enableldaptls \
 --ldapserver=dirserv.vnat --ldapbasedn="dc=example,dc=com" \
 --update
sssd を起動中:                                             [  OK  ]
oddjobd を起動中:                                          [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/sssd/sssd.conf
ldap_tls_reqcert = never
EOF
[root@linhost ~]# cat <<EOF >> /etc/nslcd.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_checkpeer no
EOF
[root@linhost ~]# service sssd restart
sssd を停止中:                                             [  OK  ]
sssd を起動中:                                             [  OK  ]

旧来の (legacy な) 手順:

[root@linhost ~]# authconfig --enableforcelegacy --enablemkhomedir \
 --enableldap --enableldapauth --enableldaptls \
 --ldapserver=dirserv.vnat --ldapbasedn="dc=example,dc=com" \
 --update
nslcd を起動中:                                            [  OK  ]
oddjobd を起動中:                                          [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/nslcd.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_reqcert no
EOF
[root@linhost ~]# service nslcd restart
nslcd を停止中:                                            [  OK  ]
nslcd を起動中:                                            [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/pam_ldap.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_checkpeer no
EOF

では、動作を確認してみます。

[root@linhost ~]# id foo
uid=1000(foo) gid=1000(foo) 所属グループ=1000(foo)
[root@linhost ~]# su - foo
Creating home directory for foo.
[foo@linhost ~]$ su - bar
パスワード: secret4
Creating home directory for bar.
[bar@linhost ~]$ id
uid=1001(bar) gid=1001(bar) 所属グループ=1001(bar) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[bar@linhost ~]$ 

良いようです。

SSH キー配布の設定

ディレクトリサーバに、SSH のキーを保持する LDAP スキーマが必要なので、oepnssh-ldap をインストールします。

[root@dirserv ~]# yum install -y openssh-ldap

slapd の設定を修正し、スキーマを追加します。キーへのアクセス権限の設定も追加しておきます。

[root@dirserv ~]# cp /etc/openldap/slapd.conf \
 /etc/openldap/slapd.conf.orig
[root@dirserv ~]# vi /etc/openldap/slapd.conf
[root@dirserv ~]# diff -uNr /etc/openldap/slapd.conf.orig \
 /etc/openldap/slapd.conf
--- /etc/openldap/slapd.conf.orig
+++ /etc/openldap/slapd.conf
@@ -15,6 +15,7 @@
 include                /etc/openldap/schema/openldap.schema
 include                /etc/openldap/schema/ppolicy.schema
 include                /etc/openldap/schema/collective.schema
+include                /usr/share/doc/openssh-ldap-5.3p1/openssh-lpk-openldap.schema

 # Allow LDAPv2 client connections.  This is NOT the default.
 allow bind_v2
@@ -101,6 +102,11 @@
   by anonymous auth
   by * none

+access to attrs=sshPublicKey
+  by dn="cn=Manager,dc=example,dc=com" write
+  by self write
+  by * none
+
 access to *
   by dn.exact="cn=Manager,dc=example,dc=com" write
   by self write
[root@dirserv ~]# service slapd restart
slapd を停止中:                                            [  OK  ]
slapd を起動中:

ここでキーを作成し、登録します。

[root@dirserv ~]# ssh-keygen -f ~/.ssh/id_rsa -N ""
Generating public/private rsa key pair.
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
(中略)
[root@dirserv ~]# key=$(cat ~/.ssh/id_rsa.pub)
[root@dirserv ~]# ldapmodify -x -D "cn=Manager,dc=example,dc=com" \
 -w secret2 <<EOF
dn: uid=foo,ou=People,o=Users,dc=example,dc=com
changetype: modify
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: $key
EOF

以上で、LDAP サーバ側は終了です。

sshd サーバの設定

sshd サーバが LDAP サーバから、対象ユーザの公開鍵を取得できるように設定します。まずは、sshd の外部プログラムを追加でインストールします。

[root@linhost ~]# yum install -y openssh-ldap

sshd が、外部プログラムからキーを取得するように設定します。上記パッケージに含まれる HOWTO.ldap-keys では "AuthorizedKeysCommand" のパラメータをダブルクォートでくくっていますが、そのように記述すると、パーサはこれを処理しないため、動きません。クォートせずに、"/" で始まる絶対パスを指定してください。なお、stat(2) でファイルの有無を確認しているので、行末にスペース等が入っていると、これまた動きません。総じて不親切です。あまり使われていないのか。

[root@linhost ~]# cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig
[root@linhost ~]# vi /etc/ssh/sshd_config
[root@linhost ~]# diff -uNr \
 /etc/ssh/sshd_config.orig \
 /etc/ssh/sshd_config
--- /etc/ssh/sshd_config.orig
+++ /etc/ssh/sshd_config
@@ -45,10 +45,10 @@
 #MaxSessions 10

 #RSAAuthentication yes
-#PubkeyAuthentication yes
+PubkeyAuthentication yes
 #AuthorizedKeysFile    .ssh/authorized_keys
-#AuthorizedKeysCommand none
-#AuthorizedKeysCommandRunAs nobody
+AuthorizedKeysCommand /usr/libexec/openssh/ssh-ldap-wrapper
+AuthorizedKeysCommandRunAs root

 # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
 #RhostsRSAAuthentication no

上記の外部プログラムが利用する設定を記述します。パスワードを含むので、パーミッションに注意してください。設定後、"/usr/libexec/openssh/ssh-ldap-wrapper foo" のように実行してキーが返って来ないようであれば、設定をしくじっています。(※注意: openssh-ldap-5.3p1-52 の ssh-ldap-wrapper には、設定ファイルのパーサにバグがあり、"tls_checkpeer" の設定が効きません。直すか、ちゃんとした証明書を用意するか、TLS をオフにするしかないようです。下記では、オフにしています)

# TLS が使えなくとも、公開鍵ならば平文で飛んでも問題ありませんから、なまじ rootdn のパスワードを指定するよりも、anonymous でキーを取得できるようにした方が良いかも知れません

[root@linhost ~]# cat > /etc/ssh/ldap.conf <<EOF
uri ldap://dirserv.vnat/
base dc=example,dc=com
binddn cn=Manager,dc=example,dc=com
bindpw secret2
host dirserv.vnat

ssl no
#ssl start_tls
tls_cacertdir /etc/openldap/cacerts
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_checkpeer no
EOF
[root@linhost ~]# chmod 600 /etc/ssh/ldap.conf

sshd を再起動します。

[root@linhost ~]# service sshd restart
sshd を停止中:                                             [  OK  ]
sshd を起動中:                                             [  OK  ]

以上です。試しに、~/.ssh/id_rsa.pub を持っているホストからログインしてみます。

[root@dirserv etc]# ssh -o RSAAuthentication=yes \
 -o PasswordAuthentication=no -l foo linhost.vnat
Last login: Thu Dec XX 15:54:49 2011 from dirserv.vnat
[foo@linhost ~]$

良いようですね。では。

2011年12月9日金曜日

prelink されたバイナリの "rpm -V" での検証は無問題

寒くなってまいりましたが、いかがお過ごしでしょうか。お疲れ様です、サイオス 那賀です。

今どきの Red Hat 系のシステムでは、デフォルトで prelink が有効になっているんですね。下記のように、cron から起動されています。

$ ls /etc/cron.daily/prelink
/etc/cron.daily/prelink*
$ grep PRELINKING /etc/sysconfig/prelink
PRELINKING=yes

定期的に prelink をしなおしているので、バイナリが書き換わっています。"libxml2" の持つ "/usr/lib64/libxml2.so.2.6.26" を見てみます。下記は、RPM のシステムが保持している MD5 値です。

$ pkg=libxml2
$ file=/usr/lib64/libxml2.so.2.6.26
$ rpm -q --dump $pkg | grep ^$file | cut -d' ' -f1,4
/usr/lib64/libxml2.so.2.6.26 0cb304449951396925c1ad4e84930a22

ファイルから直接に MD5 値を取得すると、当然 prelink が書きかえた分だけ違います。

$ md5sum $file
1dd15c495edb654f0a55c366bf39bdf7  /usr/lib64/libxml2.so.2.6.26

ところが、"rpm -V" で verify しても、何も言われないんですよね。

$ rpm -V libxml2

なぜだろうかと思ったのですが、ミソは、以下のファイルでした。

$ rpm -qf /etc/rpm/macros.prelink
prelink-0.4.0-2.el5
$ cat /etc/rpm/macros.prelink
# rpm-4.1 verifies prelinked libraries using a prelink undo helper.
#       Note: The 2nd token is used as argv[0] and "library" is a
#       placeholder that will be deleted and replaced with the appropriate
#       library file path.
%__prelink_undo_cmd     /usr/sbin/prelink prelink -y library

"rpm -V" の verify をする際に RPM は、上記マクロのコマンドを内部で呼んで、prelink による改変前のバイナリのダイジェスト値で比較して、それ以外の改変の有無を判断しています。手実行して、RPM が内部で保持している MD5 値と一致することを確認してみます。

$ prelink --verify --md5 $file
0cb304449951396925c1ad4e84930a22  /usr/lib64/libxml2.so.2.6.26

OK でした。なお、prelink の処理は可逆です。元に戻すための情報は、ELF のバイナリファイルに追加されるセクション ".gnu.prelink_undo" 内に保持されています。

$ readelf --section-headers $file | grep --after-context=1 prelink
  [27] .gnu.prelink_undo PROGBITS         0000000000000000  0013baf0
       0000000000000798  0000000000000001           0     0     8

元に戻してみます。

$ sudo prelink --undo $file
$ md5sum $file
0cb304449951396925c1ad4e84930a22  /usr/lib64/libxml2.so.2.6.26

今度は、直接 MD5 値を取っても、RPM の内部値と一致しています。

処理まで追おうとして力つきましたので、今日はこのへんで。では。

追記 (Mon Dec 12 2011)

RHEL6 系からは、ダイジェスト値が SHA1 の 256 ビットに変更されています。

$ pkg=libxml2
$ file=/usr/lib64/libxml2.so.2.6.26
$ rpm -q --dump $pkg | grep ^$file | cut -d' ' -f1,4
/usr/lib64/libxml2.so.2.7.6 2b7ae1cbfd796e3ce32a0a836d801bfcc0feefad284705c572d9bb5528f387af
$ sha256sum $file
292596e8550d3167df658b03578654337c26c99d37fc10beb0771385b33d426e  /usr/lib64/libxml2.so.2.7.6
$ prelink --verify $file | sha256sum
2b7ae1cbfd796e3ce32a0a836d801bfcc0feefad284705c572d9bb5528f387af  -

ダイジェスト値はパッケージを作成した時点で埋めこまれます。rpm-*/build/files.c:genCpioListAndHeader() あたりを見ると分かりますが、%_binary_filedigest_algorithm マクロが得られなければ、デフォルトで MD5 になるようになっていますので、明示的に SHA256 が指定されているはずです。これは、redhat-rpm-config パッケージに含まれる /usr/lib/rpm/redhat/macros で指定されています。

...
# Use SHA-256 for FILEDIGESTS instead of default MD5
%_source_filedigest_algorithm 8
%_binary_filedigest_algorithm 8
...

2011年12月8日木曜日

ソースの入手と再ビルド & デバッグ ~ Mac OS X Homebrew 編

お疲れ様です、サイオス 那賀です。

はじめて UNIX OS を買いました、Mac OS X です。そこで、「ソースの入手と再ビルド ~ CentOS / Scientific Linux 編」、「ソースの入手と再ビルド ~ Ubuntu 編」に続いて、Mac OS X 上での UNIX (POSIX?) 系パッケージの管理について簡単にお話ししようと思います。

Mac OS X 上での後入れの UNIX 系パッケージの管理には、今回 Homebrew を利用することにしました。Homebrew は、BSD 系のシステムで多く用いられているパッケージ管理システムである ports 系の一派であり、FreeBSD Ports, Gentoo Portage, MacPorts (旧 DarwinPorts) などの仲間です。

RPM (Red Hat) 系や deb (Debian) 系のパッケージ管理では一般的に、ディストリビューション開発者の側でビルドされたバイナリパッケージを入手してインストールする方式を採るのに対して、ports 系のパッケージは、ネットワークからソースコードを取得してきて、自分のホスト上で自前でビルドをするという特徴を持ちます (バイナリ配布もできるようにはなっていますけどね)。ターゲット・ホスト上には、インストール可能なパッケージのソースの所在をあらわす URL とビルド手順が書かれたファイルが格納されており、それらに従って、ホスト上で都度コンパイルをしながら、インストールを行います。もちろん、パッケージ管理ツールとしては当然のこととして、パッケージ単位でのインストール、アンインストール、ファイルのリスト管理、依存関係の解決などの機能を備えています。

ports 系のメリットとしては、OS や CPU フィーチャごとに最高の最適化がかけられる、やっていることが分かりやすい、などが挙げられる反面、環境ごとにバイナリが異なるのでサポートがしづらい、ネットワーク環境が必須、ビルドにマシンパワーと長い時間を要する、依存される度合いの大きいライブラリの API が更新されると大変、使っているだけで変態扱いされる、等のデメリットもあります。「貴重な一台をマルチユースで大事に使う」という旧来型のコンピューティング・シーンにおいては快適ですが、昨今の、安価なサーバを次から次に使い捨て、クラウドでガンガンとスケールするような使い方とは、相性が悪いのではないかなぁと思います。

さて、私の Mac の使い方は、当然前者の「貴重な一台をマルチユースで大事に使う」スタイルですので、ジャンジャン Homebrew を活用しましょう。従来 Mac OS X 環境で広く用いられてきた MacPorts が、すでに OS X の標準アプリケーションとして /bin/, /sbin/, /usr/bin/ 等にインストールされているソフトウェアも、重複して MacPorts の管理下でインストールしなければならず、無用に CPU パワーと初期導入時間を要し、別バージョンの多重管理に起因する問題を起こしたりしていたのに対して、Homebrew では、重複しないものだけを、/usr/local/ の下で完結して管理するところに、非常に好感が持てます。

では、ソースの入手とビルド手順を解説して行きましょう…かと思ったのですが、正直なところ「Formula Cookbook - GitHub」を読んで /usr/local/Library/Formula/ 以下の formula ファイルを眺めれば、Homebrew がやっていることについては誰にでも分かると思いますので、そちらに譲ります。

では。

…と、それだけでは何なので、フルにデバッグ情報を積んで、ソースレベル・デバッグが可能な状態でのインストール方法でも紹介しようと思います。

/usr/local/Library/Homebrew/formula.rb:Formula#mktemp() におけるビルドディレクトリの処理を見ると、インストール終了後は "ensure" で、何が何でもビルドツリーを消しに行きますので、ソースファイルとオブジェクトファイルを消さずに残すことができません。private だし。

private
  def mktemp
    tmp_prefix = ENV['HOMEBREW_TEMP'] || '/tmp'
    tmp=Pathname.new `/usr/bin/mktemp -d #{tmp_prefix}/homebrew-#{name}-#{version}-XXXX`.strip
    raise "Couldn't create build sandbox" if not tmp.directory? or $? != 0
    begin
      wd=Dir.pwd
      Dir.chdir tmp
      yield
    ensure
      Dir.chdir wd
      tmp.rmtree
    end
  end

仕方が無いので、Cellar ディレクトリ内で直接ビルドをするよう、Formula ファイルを以下のように修正します。(RPM の debuginfo が、ビルドした場所と違うディレクトリにソースを格納できるのは、debugedit コマンドで、ELF 内の DWARF 情報を直接書き換えという大技を使えるからです。Mac OS X の Mach-O バイナリでは、そもそも DWARF 情報を *.o が持っていて、実行ファイルは、それらへのリンクしか持っていないんですね。それらがすべてフルパスで格納されているので、どうやってソースを移動して良いやらさっぱり分からないので、やめておきます)

$ brew edit wget
$ (cd /usr/local/; git diff Library/Formula/wget.rb)
diff --git a/Library/Formula/wget.rb b/Library/Formula/wget.rb
index 66240e7..26c3328 100644
--- a/Library/Formula/wget.rb
+++ b/Library/Formula/wget.rb
@@ -13,6 +13,8 @@ class Wget < Formula
     [["--enable-iri", "Enable iri support."]]
   end

+  skip_clean :all if ARGV.debug? # strip を抑制します
+
   def install
     args = ["--disable-debug",
             "--prefix=#{prefix}",
@@ -20,6 +22,14 @@ class Wget < Formula

     args << "--disable-iri" unless ARGV.include? "--enable-iri"

+    if ARGV.debug?
+      ENV.Og # 最適化を切ります → "-O0"
+      d = Dir.getwd
+      Dir.chdir ".."
+      mv d, prefix + 'src' # ソースをインストール対象とします
+      Dir.chdir prefix + 'src' # その中でビルドします
+    end
+
     system "./configure", *args
     system "make install"
   end

上記の修正の後、"--debug" オプションをつけてインストールすれば、最適化が無効になり、ソースレベルのデバッグが有効になります。

$ brew install --debug --force wget
$ gdb wget
GNU gdb 6.3.50-20050815 (Apple version gdb-1708) (Thu Nov XX 21:59:02 UTC 2011)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...
Executing: ~/etc/gdbinit.
Done: ~/etc/gdbinit.
Reading symbols for shared libraries ..... done

(gdb) b main
Breakpoint 1 at 0x10002bef2: file main.c, line 915.
(gdb) run
Starting program: /usr/local/bin/wget
Reading symbols for shared libraries ++++......................... done

Breakpoint 1, main (argc=1, argv=0x7fff5fbff998) at main.c:915
915       bool append_to_log = false;
(gdb) l
910     main (int argc, char **argv)
911     {
912       char **url, **t;
913       int i, ret, longindex;
914       int nurl;
915       bool append_to_log = false;
916
917       total_downloaded_bytes = 0;
918
919       program_name = argv[0];
(gdb)

Formula ファイルは Git で管理されていますので、修正を加えても、コンフリクトしない限りは問題なくアップデートができます。

$ brew update

ところで、Homebrew の用いる用語のアナロジー―homebrew (自家醸造), formula (醸造法), cellar (酒蔵), keg (樽)―等は、とても秀逸だと思います。ではまた。

2011年12月7日水曜日

RHEL6 系での OpenLDAP サーバと PAM 認証

お世話になっております、SIOS 那賀です。

Scientific Linux 6.1 で OpenLDAP のサーバを立てようとして、はたと止まってしまいました。RHEL5 系まででお馴染みの /etc/openldap/slapd.conf がありません。どうやら RHEL6 からは、以前は slapd.conf に記述していたメタデータも、論理的には DN "cn=config" のノードの下に、物理的には /etc/openldap/slapd.d/ 以下に、LDIF を用いた「dynamic runtime configuration engine」なるデータ構造で格納されるように変更されたようです。それに伴い他のノードと同様、汎用の DIT 操作コマンドを用いて、しかも動的に設定できるようになっています。詳しくは RHEL6 の「移行管理ガイド」の「7.3.1 slapd 設定を変換する」や、本家ドキュメントの「OpenLDAP Software 2.4 Administrator's Guide: Configuring slapd」等を参照してください。

追記 (Mon Dec 12 2011): こちらの記事では slapd.d/ で頑張っていましたが、面倒になってしまい slapd.conf に戻ってしまった記事はこちら→「SIOS OSS Tech: RHEL6.1~系の OpenLDAP (slapd.conf) で SSH 公開鍵配布」。

RHEL6 系の初期 slapd.d/ は何のため?

さて、ここで一旦、少々脇道にそれます。上記のサイトを参照すると、既存の slapd.conf があるならば、slaptest コマンドで変換して slapd.d/ を作れば良いように読めますが、では、新規インストールをした場合にはどうすれば良いのでしょう? アップデートではなく新規で openldap-servers パッケージを入れても、以下のように、ちゃんとデータが構築されているように見えます。

[root@dirserv ~]# find /etc/openldap/slapd.d/
/etc/openldap/slapd.d/
/etc/openldap/slapd.d/cn=config.ldif
/etc/openldap/slapd.d/cn=config
/etc/openldap/slapd.d/cn=config/olcDatabase={-1}frontend.ldif
/etc/openldap/slapd.d/cn=config/olcDatabase={2}monitor.ldif
/etc/openldap/slapd.d/cn=config/olcDatabase={0}config.ldif
/etc/openldap/slapd.d/cn=config/cn=schema
/etc/openldap/slapd.d/cn=config/cn=schema/cn={11}collective.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={10}ppolicy.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={3}duaconf.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={9}openldap.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={0}corba.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={7}misc.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={2}cosine.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={6}java.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={4}dyngroup.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={5}inetorgperson.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={8}nis.ldif
/etc/openldap/slapd.d/cn=config/cn=schema/cn={1}core.ldif
/etc/openldap/slapd.d/cn=config/cn=schema.ldif
/etc/openldap/slapd.d/cn=config/olcDatabase={1}bdb.ldif

しかしこのデータ、アクセスしようにも、rootdn のパスワードが分かりません。RHEL の文書を見ても書かれていないようです。以下のように、ファイルも誰の持ち物でもありません。

[root@dirserv ~]# rpm -qf /etc/openldap/slapd.d/*
file /etc/openldap/slapd.d/cn=config is not owned by any package
file /etc/openldap/slapd.d/cn=config.ldif is not owned by any package

ということは、インストールの際に動的に作成しているのでしょう。%postinstall のスクリプトを見てみます。

[root@dirserv ~]# rpm -q --scripts openldap-servers
(中略。↓ インストール後のスクリプトにおいて、)
postinstall scriptlet (using /bin/sh):
(中略。↓ slapd.d/ ディレクトリが存在しなければ、)
# generate configuration in slapd.d
if ! ls -d /etc/openldap/slapd.d/* &>/dev/null; then
    (中略。↓ slapd.conf.obsolete なるファイルをコピーしてきて、)
    [ $fresh_install -eq 0 ] && \
        cp /usr/share/openldap-servers/slapd.conf.obsolete \
         /etc/openldap/slapd.conf
    (中略。↓ config 用のエントリを加えて、)
    cat >> /etc/openldap/slapd.conf << EOF
database config
rootdn   "cn=admin,cn=config"
#rootpw   secret
EOF
    (中略。slaptest コマンドで slapd.d/ 形式に変換します。)
    slaptest -f /etc/openldap/slapd.conf \
     -F /etc/openldap/slapd.d > /dev/null 2> /dev/null

上記のとおり、「#rootpw secret」となっています。パスワードは未設定です。これではアクセスできませんね。

さらに余談として、Ubuntu でも rootdn のパスワードは設定されていないのですが、かわりに、root ユーザで SASL EXTERNAL 認証でアクセスすると、何でもできるように考慮されています。Ubuntu 10.04 サーバガイドの「OpenLDAP Server」の項などを見ると、SASL EXTERNAL の接続でサクサクと rootpw を設定しているのに、なぜ RHEL で同じことができないのかと思えば、そういうことでした。

debian/slapd.init.ldif:

# Frontend settings
dn: olcDatabase={-1}frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: {-1}frontend
(中略)
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break

debian/slapd.install:

debian/slapd.init.ldif usr/share/slapd:

debian/slapd.postinst:

create_new_configuration() {
  (中略)
  init_ldif="/usr/share/slapd/slapd.init.ldif"
  (中略)
  capture_diagnostics slapadd -F "${SLAPD_CONF}" \
   -b "cn=config" -l ${init_ldif} || failed=1

もっとも Ubuntu でも、従来 (2.4.21-0ubuntu5 以前) はこの点が考慮されていなかったらしく、アップデートの際には既存のデータに対して、下記のように sed で内部表現をゴリゴリと書き換えているんですけれどね。

debian/slapd.postinst:

postinst_upgrade_configuration() {
  (中略)
  # Grant manage access to connections made by the root user via
  # SASL EXTERNAL
  if previous_version_older 2.4.21-0ubuntu5 ; then
    if [ -d "$SLAPD_CONF" ]; then
      # Stick the new olcAccess at the begining of the
      # olcAccess list (using an index of 0 *and*
      # adding it as early as possible in the ldif file)
      # to make sure that local root has access to the
      # database no matter what other acls say.
      sed -i 's/^\(olcDatabase: {-1}frontend\)/\0\nolcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break/' "${SLAPD_CONF}/cn=config/olcDatabase={-1}frontend.ldif"
      sed -i 's/^\(olcDatabase: {0}config\)/\0\nolcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break/' "${SLAPD_CONF}/cn=config/olcDatabase={0}config.ldif"
    fi
  fi

これ同様に sed で書き換えるなり、以下のように slapd.d/cn=config/olcDatabase={0}config.ldif に "olcRootPW" のエントリをねじ込むなりしてサービスを上げ直せば、パスワードを設定することは可能です。これは、いよいよの「rootdn のパスワード忘れちゃった!」の際にも役に立つでしょう。

[root@dirserv ~]# service slapd stop
Stopping slapd:                                            [  OK  ]
[root@dirserv ~]# slappasswd -s secret1
{SSHA}QVHYz50xBxpbbEa5qO+5cn7jDktBVXr4
[root@dirserv ~]# echo 'olcRootPW:{SSHA}QVHYz50xBxpbbEa5qO+5cn7jDktBVXr4' >> /etc/openldap/slapd.d/cn\=config/olcDatabase\=\{0\}config.ldif
[root@dirserv ~]# service slapd startx
Starting slapd:                                            [  OK  ]
[root@dirserv ~]# ldapsearch -x -D "cn=admin,cn=config" -b "cn=config" -w secret1 "(olcRootDN=*)"
# extended LDIF
#
# LDAPv3
# base <cn=config> with scope subtree
# filter: (olcRootPW=*)
# requesting: ALL
#

# {0}config, config
dn: olcDatabase={0}config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: {0}config
olcAccess: {0}to *  by * none
olcAddContentAcl: TRUE
olcLastMod: TRUE
olcMaxDerefDepth: 15
olcReadOnly: FALSE
olcRootDN: cn=admin,cn=config
olcRootPW: {SSHA}QVHYz50xBxpbbEa5qO+5cn7jDktBVXr4
olcSyncUseSubentry: FALSE
olcMonitoring: FALSE

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

ではなぜ RHEL6 では、アクセス手段のない slapd.d/ をわざわざ作っているのでしょうか? 実は最新の OpenLDAP の slapd でも、slapd.d/ が無いところへユーザが slapd.conf を置けば、従来と同様に slapd は slapd.conf から設定を読み込んでしまうのです (詳しくは openldap-*/servers/slapd/bconfig.c の read_config() 関数を読んでください)。よって、新規の際の slapd.d/ は、従来の slapd.conf 流の設定をあっさりとは使わせないようにするために、教育的見地からダミーとして置かれているものと見て良いでしょう。

さて、以上のようにデフォルトの slapd.d/ が単なるダミーであることが分かりましたし、本家の文書Red Hat の文書も共に、slapd.d/ 以下を手で編集するのはイカン、お行儀が悪いと言っておりますので、何ら躊躇することなく slapd.d/ 以下を削除して、自前の slapd.conf から slaptest コマンドで slapd.d/ を新規生成することにしましょう。

閑話休題、やっと本題に入ります。

サーバの設定

まずはいつものように、SELinux と iptables の無効化です。実運用のサーバでは、運用ポリシーに従って、それぞれ適切に設定してください。

[root@dirserv ~]# setenforce permissive
[root@dirserv ~]# getenforce
Permissive
[root@dirserv ~]# service iptables stop
iptables: ファイアウォールルールを消去中:                  [  OK  ]
iptables: チェインをポリシー ACCEPT へ設定中filter         [  OK  ]
iptables: モジュールを取り外し中:                          [  OK  ]
[root@dirserv ~]#

サーバと、操作用のクライアントをインストールします。

[root@dirserv ~]# yum install -y openldap-clients openldap-servers

"Obsolete" のファイルを元に、slapd.conf を作成します。

[root@dirserv ~]# slappasswd -s secret2
{SSHA}Z42Qdb/r/YAw0QRD8vI/SYcLgoxtzF4l
[root@dirserv ~]# cp /usr/share/openldap-servers/slapd.conf.obsolete slapd.conf
[root@dirserv ~]# vi slapd.conf
[root@dirserv ~]# diff -uNr /usr/share/openldap-servers/slapd.conf.obsolete slapd.conf
--- /usr/share/openldap-servers/slapd.conf.obsolete
+++ slapd.conf
@@ -64,9 +64,9 @@
 # /etc/pki/tls/certs, running "make slapd.pem", and fixing permissions on
 # slapd.pem so that the ldap user or group can read it.  Your client software
 # may balk at self-signed certificates, however.
-# TLSCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
-# TLSCertificateFile /etc/pki/tls/certs/slapd.pem
-# TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem
+TLSCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
+TLSCertificateFile /etc/pki/tls/certs/slapd.pem
+TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem

 # Sample security restrictions
 #      Require integrity protection (prevent hijacking)
@@ -95,14 +95,30 @@
 #
 # rootdn can always read and write EVERYTHING!

+access to attrs=userPassword
+  by dn="cn=Manager,dc=example,dc=com" write
+  by self write
+  by anonymous auth
+  by * none
+
+access to *
+  by dn.exact="cn=Manager,dc=example,dc=com" write
+  by self write
+  by * read
+
 #######################################################################
 # ldbm and/or bdb database definitions
 #######################################################################

+database       config
+rootdn         "cn=admin,cn=config"
+rootpw         {SSHA}QVHYz50xBxpbbEa5qO+5cn7jDktBVXr4
+
 database       bdb
-suffix         "dc=my-domain,dc=com"
+suffix         "dc=example,dc=com"
 checkpoint     1024 15
-rootdn         "cn=Manager,dc=my-domain,dc=com"
+rootdn         "cn=Manager,dc=example,dc=com"
+rootpw         {SSHA}Z42Qdb/r/YAw0QRD8vI/SYcLgoxtzF4l
 # Cleartext passwords, especially for the rootdn, should
 # be avoided.  See slappasswd(8) and slapd.conf(5) for details.
 # Use of strong authentication encouraged.

(動いているようであれば) サービスを停止し、現在のデータを削除してから、新しいデータを作り、再度起動します。今回はテストなので、インデックスも BDB の設定も、そのまま放置します。実運用では、パフォーマンスやバックアップにも気をつかってあげてください。

[root@dirserv ~]# service slapd stop
slapd を停止中:                                            [失敗]
[root@dirserv ~]# rm -fr /etc/openldap/slapd.d/* /var/lib/ldap/*
[root@dirserv ~]# slaptest -f slapd.conf -F /etc/openldap/slapd.d/
bdb_db_open: warning - no DB_CONFIG file found in directory /var/lib/ldap: (2).
Expect poor performance for suffix "dc=example,dc=com".
bdb_db_open: database "dc=example,dc=com": db_open(/var/lib/ldap/id2entry.bdb) failed: No such file or directory (2).
backend_startup_one (type=bdb, suffix="dc=example,dc=com"): bi_db_open failed! (2)
slap_startup failed (test would succeed using the -u switch)
[root@dirserv ~]# chown -R ldap.ldap /etc/openldap/slapd.d/* /var/lib/ldap/*
[root@dirserv ~]# service slapd start
slapd を起動中:                                            [  OK  ]

以下は任意ですが、サーバの挙動が分かりやすいように、ログの出力も設定しておきます。slapd は local4 のファシリティで syslog 出力をするのですが、せっかくの RHEL6 系の rsyslog ですので、アプリケーション名 ("slapd") でログを振り分けるようにしてみます。実運用では、ログが溢れないように、ローテートの設定もしておいてください。

[root@dirserv ~]# cp /etc/rsyslog.conf /etc/rsyslog.conf.orig
[root@dirserv ~]# vi /etc/rsyslog.conf
[root@dirserv ~]# diff -uNr /etc/rsyslog.conf.orig /etc/rsyslog.conf
--- /etc/rsyslog.conf.orig
+++ /etc/rsyslog.conf
@@ -76,3 +76,6 @@
 # remote host is: name/ip:port, e.g. 192.168.0.1:514, port optional
 #*.* @@remote-host:514
 # ### end of the forwarding rule ###
+
+!slapd
+*.* /var/log/slapd.log
[root@dirserv ~]# service rsyslog reload
Reloading system logger...                                 [  OK  ]

アクセスできるかどうか、試してみます。

[root@dirserv ~]# ldapsearch -x -D "cn=admin,cn=config" -b "cn=config" -w secret1 "(olcRootDN=*)"
[root@dirserv ~]# ldapsearch -x -D "cn=Manager,dc=example,dc=com" -b "dc=example,dc=com" -w secret2

良いようであれば、認証用のデータを入れて行きます。根元から作って行きます。

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret2 <<EOF
dn: dc=example,dc=com
dc: example
objectClass: dcObject
objectClass: organization
o: Example Corp.

dn: cn=Manager,dc=example,dc=com
cn: Manager
objectClass: organizationalRole

dn: o=Users,dc=example,dc=com
o: Linux Users
objectClass: organization

dn: ou=People,o=Users,dc=example,dc=com
ou: People
objectClass: organizationalUnit

dn: ou=Group,o=Users,dc=example,dc=com
ou: Group
objectClass: organizationalUnit
EOF
adding new entry "dc=example,dc=com"

adding new entry "cn=Manager,dc=example,dc=com"

adding new entry "o=Users,dc=example,dc=com"

adding new entry "ou=People,o=Users,dc=example,dc=com"

adding new entry "ou=Group,o=Users,dc=example,dc=com"

ちょっと間違うと、途中まで作ったところでエラーになり、そこまでの分が半端に残ります。failure atomic ではありません…、トランザクションがあればいいのにね。今回はテストですので、そんな時は、以下のように根元からガバッと再帰で消してしまってください。

[root@dirserv ~]# ldapdelete -v -x -D "cn=Manager,dc=example,dc=com" -w secret2 -r "dc=example,dc=com"

ユーザ (foo と bar) を追加します。OpenLDAP のクライアント側 (SSSD, pam_ldap, nss-pam-ldapd 等) は、文書にはあまりキチンと書かれていないのですが (せいぜい /usr/share/doc/nss-pam-ldapd-0.7.5/README くらいか)、デフォルトで、クラス "posixAccount" や "posixShadow" を持つノードを、ユーザ情報として取りにきます。ですので、実はツリー構造は割と恣意的に決めてしまっても問題ありません。

[root@dirserv ~]# slappasswd -s secret3
{SSHA}LAs1y8ozcRtUNMVZh3Sj+t8Vn/48HJV3
[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret2 <<EOF
dn: uid=foo,ou=People,o=Users,dc=example,dc=com
objectClass: top
objectClass: posixAccount
objectClass: account
gecos: ldap system users
cn: foo
uid: foo
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/foo
loginShell: /bin/bash
userPassword: {SSHA}LAs1y8ozcRtUNMVZh3Sj+t8Vn/48HJV3
EOF
adding new entry "uid=foo,ou=People,o=Users,dc=example,dc=com"

[root@dirserv ~]# slappasswd -s secret4
{SSHA}8XOqcgEYQjI9vFreV8j6dc6B6Fjovfts
[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret2 <<EOF
dn: uid=bar,ou=People,o=Users,dc=example,dc=com
objectClass: top
objectClass: posixAccount
objectClass: account
gecos: ldap system users
cn: bar
uid: bar
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/bar
loginShell: /bin/bash
userPassword: {SSHA}8XOqcgEYQjI9vFreV8j6dc6B6Fjovfts
EOF
adding new entry "uid=bar,ou=People,o=Users,dc=example,dc=com"

グループ (foo と bar) を追加します。こちらも同様に、クライアントはデフォルトで、クラス "posixGroup" を持つノードを探索に来ます。今回は、昨今の Linux ディストリビューションの構成と同様に、UPG (User Private Group) 式で、ユーザごとに個別に同名のグループを割り当てますが、LDAP 的には、共通のグループを割り当てる方が一般的かも知れません。

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret2 <<EOF
dn: cn=foo,ou=Group,o=Users,dc=example,dc=com
cn: foo
objectClass: posixGroup
gidNumber: 1000
userPassword: {CRYPT}x
EOF
adding new entry "cn=foo,ou=Group,o=Users,dc=example,dc=com"

[root@dirserv ~]# ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret2 <<EOF
dn: cn=bar,ou=Group,o=Users,dc=example,dc=com
cn: bar
objectClass: posixGroup
gidNumber: 1001
userPassword: {CRYPT}x
EOF
adding new entry "cn=bar,ou=Group,o=Users,dc=example,dc=com"

一般ユーザから、他人のパスワードが見えないことを確認しておきます。

[root@dirserv ~]# ldapsearch -x -D "uid=foo,ou=People,o=Users,dc=example,dc=com" -b "dc=example,dc=com" -w secret3 "(objectClass=posixAccount)"
[root@dirserv ~]# ldapsearch -x -D "uid=bar,ou=People,o=Users,dc=example,dc=com" -b "dc=example,dc=com" -w secret4 "(objectClass=posixAccount)"

認証依頼元の Linux ホストの設定

こちらも同様に、SELinux と iptables の無効化です。実運用のサー (以下テンプレ略)。

[root@linhost ~]# setenforce permissive
[root@linhost ~]# getenforce
Permissive
[root@linhost ~]# service iptables stop
iptables: ファイアウォールルールを消去中:                  [  OK  ]
iptables: チェインをポリシー ACCEPT へ設定中filter         [  OK  ]
iptables: モジュールを取り外し中:                          [  OK  ]
[root@linhost ~]#

RHEL5 系の nss_ldap から、RHEL6 系では PAM は pam_ldap に、NSS は nss_ldap からフォークした nss-pam-ldapd へと分離しており、また、それらを束ねる SSSD 抽象化レイヤ (詳細) が入っていたりと、設定ファイルが多岐に渡り、もはや手作業では手に負えなくなっているので、authconfig コマンドを使用します。

同時に、TLS (389 番ポートでの "STARTTLS" による接続) も設定します。authconfig では、認証局ナシでの TLS 設定ができないので、設定をした後で、ちょっとだけ手を入れる必要があります。

SSSD を用いる手順:

[root@linhost ~]# authconfig --disableforcelegacy --enablemkhomedir --enableldap --enableldapauth --enableldaptls --ldapserver=dirserv.vnat --ldapbasedn="dc=example,dc=com" --update
sssd を起動中:                                             [  OK  ]
oddjobd を起動中:                                          [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/sssd/sssd.conf
ldap_tls_reqcert = never
EOF
[root@linhost ~]# cat <<EOF >> /etc/nslcd.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_checkpeer no
EOF
[root@linhost ~]# service sssd restart
sssd を停止中:                                             [  OK  ]
sssd を起動中:                                             [  OK  ]

旧来の (legacy な) 手順:

[root@linhost ~]# authconfig --enableforcelegacy --enablemkhomedir --enableldap --enableldapauth --enableldaptls --ldapserver=dirserv.vnat --ldapbasedn="dc=example,dc=com" --update
nslcd を起動中:                                            [  OK  ]
oddjobd を起動中:                                          [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/nslcd.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_reqcert no
EOF
[root@linhost ~]# service nslcd restart
nslcd を停止中:                                            [  OK  ]
nslcd を起動中:                                            [  OK  ]
[root@linhost ~]# cat <<EOF >> /etc/pam_ldap.conf
tls_cacertfile /etc/pki/tls/certs/ca-bundle.crt
tls_checkpeer no
EOF

では、動作を確認してみます。

[root@linhost ~]# id foo
uid=1000(foo) gid=1000(foo) 所属グループ=1000(foo)
[root@linhost ~]# su - foo
Creating home directory for foo.
[foo@linhost ~]$ su - bar
パスワード: secret4
Creating home directory for bar.
[bar@linhost ~]$ id
uid=1001(bar) gid=1001(bar) 所属グループ=1001(bar) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[bar@linhost ~]$ 

良いようですね。では。

2011年12月6日火曜日

debuginfo RPM パッケージで、ソースレベル・デバッグをする

お元気ですか? あいにく風邪気味の、サイオス 那賀です。

最近の RPM ベースの Linux ディストリビューションであれば、バイナリ RPM と共に、debuginfo パッケージが提供されているのが普通だと思います。この debuginfo パッケージをインストールすることで、従来であればバイナリパッケージ作成の際に strip で落とされてしまっていたデバッグ情報やソース情報を別パッケージとして利用することができるようになるため、本体パッケージを肥大化させることなく、本体パッケージのバイナリそのものを用いて、ソースレベルでのデバッグを行うことができます (GDB の分離デバッグ情報についての詳細は「Separate Debug Files - Debugging with GDB」あたりを、RPM のビルド中の処理については /usr/lib/rpm/find-debuginfo.sh の debugedit のあたりを参照してください)。

しかし現在の提供形態だと、いくつか問題があります。

  • バイナリは "-g -O2" でコンパイルされているので、最適化がかかっており、デバッグがしづらい
  • 自前でパラメータを変えてビルドしたら、既存の debuginfo は使用できない
  • 最新のバイナリのアップデートは公開されているのに、対応する debuginfo が出ていないことがある (特に CentOS)
  • debuginfo パッケージは大きいので、SRPM ファイルともども、ミラーから削除されがちで入手できなかったりする (特に CentOS)

そこで、自前でパッケージをビルドして、debuginfo を活用する方法をご紹介します。以下は、おそらく RHEL4~ の系列では動きますが、それ以外はよく知りません。合わせて以前の記事も参考にしてください → 「SIOS OSS Tech: ソースの入手と再ビルド ~ CentOS / Scientific Linux 編」。

まずは、redhat-rpm-config パッケージをインストールします。debuginfo パッケージ生成関連のマクロはこのパッケージに含まれているため、これをインストールしておかないと、RPM パッケージのビルドはできますが、debuginfo パッケージが生成されません。

[root@centos4 ~]# yum install redhat-rpm-config

次にビルドです。"rpmbuild --rebuild <SRPM ファイル>" なり "rpmbuild -ba <SPEC ファイル>" なり (渋いところでは、"rpmbuild -ta <tar.gz ファイル>" なり) でビルドをするところですが、ここで、デバッグ用のオプションを指定することができます。デフォルトは "-g -O2" になっていると思いますが、特にこの "-O2" のおかげでかなり最適化がされてしまい、ソースレベルでのデバッグがしづらくなります。これを "-O0" にして最適化を切ると同時に、"-pg", "-g3" で、付加的なデバッグ情報も付けておきます。

注意: 最適化オプションやコンパイル環境の変更によって、調査するはずだった不具合が解消してしまうことも、よくあります。困ります

以下は、CentOS 4 での例です。ソースの入手からビルド~ソースレベル・デバッグまでを見てみます。

[root@centos4 ~]# wget http://ftp.redhat.com/pub/redhat/linux/enterprise/4/en/os/i386/SRPMS/bash-3.0-19.2.src.rpm
(中略)
[root@centos4 ~]# rpmbuild --rebuild \
 --define="optflags -pg -g3 -O0" bash-3.0-19.2.src.rpm
(中略)
Wrote: /usr/src/redhat/RPMS/i386/bash-3.0-19.2.i386.rpm
Wrote: /usr/src/redhat/RPMS/i386/bash-debuginfo-3.0-19.2.i386.rpm
(中略)
[root@centos4 ~]# rpm -Uvh --force \
 /usr/src/redhat/RPMS/i386/bash-3.0-19.2.i386.rpm \
 /usr/src/redhat/RPMS/i386/bash-debuginfo-3.0-19.2.i386.rpm
(中略)
[root@centos4 ~]# bash -c "while true; do sleep 1; done" &
[1] 32108
[root@centos4 ~]# gdb -p 32108
(中略)
Attaching to process 32108
Reading symbols from /bin/bash...Reading symbols from
 /usr/lib/debug/bin/bash.debug...done.
Using host libthread_db library "/lib/tls/libthread_db.so.1".
done.
Reading symbols from /lib/libtermcap.so.2...done.
Loaded symbols for /lib/libtermcap.so.2
Reading symbols from /lib/libdl.so.2...done.
Loaded symbols for /lib/libdl.so.2
Reading symbols from /lib/tls/libc.so.6...done.
Loaded symbols for /lib/tls/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
0x0027f7a2 in _dl_sysinfo_int80 () from /lib/ld-linux.so.2
(gdb) bt
#0  0x0027f7a2 in _dl_sysinfo_int80 () from /lib/ld-linux.so.2
#1  0x00324533 in __waitpid_nocancel () from /lib/tls/libc.so.6
#2  0x0807d03f in waitchld (wpid=32134, block=1) at jobs.c:2497
#3  0x0807be1d in wait_for (pid=32134) at jobs.c:1904
#4  0x0806b901 in execute_command_internal (command=0x9c73280, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x9c73378) at execute_cmd.c:704
#5  0x0806b279 in execute_command (command=0x9c73280) at execute_cmd.c:351
#6  0x0806e2db in execute_while_or_until (while_command=0x9c732d0, type=0) at execute_cmd.c:2327
#7  0x0806e1ef in execute_while_command (while_command=0x9c732d0) at execute_cmd.c:2273
#8  0x0806ba79 in execute_command_internal (command=0x9c732e0, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x9c732f8) at execute_cmd.c:764
#9  0x080a7187 in parse_and_execute (string=0x9c72d00 "while true; do sleep 1; done", from_file=0x80d9392 "-c", flags=4) at evalstring.c:267
#10 0x0805cb6f in run_one_command (command=0xbff9bc18 "while true; do sleep 1; done") at shell.c:1269
#11 0x0805bd7a in main (argc=3, argv=0xbff378e4, env=0xbff378f4) at shell.c:653
(gdb) frame 11
#11 0x0805bd7a in main (argc=3, argv=0xbff378e4, env=0xbff378f4) at shell.c:653
653           run_one_command (command_execution_string);
(gdb) l
648           if (debugging_mode)
649             start_debugger ();
650
651     #if defined (ONESHOT)
652           executing = 1;
653           run_one_command (command_execution_string);
654           exit_shell (last_command_exit_value);
655     #else /* ONESHOT */
656           with_input_from_string (command_execution_string, "-c");
657           goto read_and_execute;
(gdb) p command_execution_string
$1 = 0xbff9bc18 "while true; do sleep 1; done"

ソースもシンボルも見えています。良いようですね。

では。

話は逸れますが、上記で、上書きでインストールをする際に "rpm" コマンドに "-U" と "--force" を組み合わせています。"--force" は依存を壊しませんので、それほど問題ではありません。一方、"--nodeps" はいけません。RPM の本質は依存関係の解決だと思いますので、それを破壊する "--nodeps" は悪です。"--nodeps" を付けなければならない状況に陥ったら、開発者 or 管理者として負けだと思ってください。

2011年12月5日月曜日

RPM ベースで Puppet を動かしてみる

どうも、サイオス 那賀です。

Puppet は、構内ホストをクロスプラットフォームに集中管理するためのソフトウェアです。ユーザやパッケージの存否、設定等々を、複数のノードに一括して適用することができるようになります。

Scientific Linux 6.1 において、ごく簡単な動作を確認するまでの手順を紹介します。ここでは puppet master 側のホストの FQDN を "master.vnat", puppet 側を "node1.vnat" とします。

まず、固有のパッケージをインストールするところまでは、master, node1 とも共通です。

SELinux は permissive, もしくは disabled にします。現行のパッケージ (puppet-2.6.12-1.el6) では、enforcing だとエラーになります (Bug 726061 – puppetmaster fails on startup due to missing SSL/CA directory)。

[root@master ~]# setenforce permissive
[root@master ~]# getenforce
Permissive

デフォルトのポート番号が分からないので、iptables も、ひとまず切っておきます (8139 かな? 何その Realtek の NIC チップみたいな番号は)。

[root@master ~]# service iptables stop

パッケージは EPEL から入れますので、レポジトリ設定を入れておきます。ちなみに EPEL (Extra Packages for Enterprise Linux) とは、Fedora を元にして RHEL が作られる際に取り込まれなかったパッケージ群を、RHEL やその互換ディストリビューションのために有志が提供してくださっているプロジェクトです。

[root@master ~]# rpm -Uvh http://download.fedora.redhat.com/pub/epel/6/x86_64/epel-release-6-5.noarch.rpm

ここから、puppet master と puppet とで、インストールするパッケージが異なってきます。まず、puppet master です。"puppet-server" パッケージをインストールし、puppet master サービスを起動します。

[root@master ~]# yum install -y puppet-server
[root@master ~]# service puppetmaster start

次に puppet の側です。"puppet" パッケージを入れます。

[root@node1 ~]# yum install -y puppet

puppet が puppet master を指すように設定します。この設定ファイルは init のスクリプトに渡すものですので、/etc/sysconfig/ 以下に入っています。

[root@node1 ~]# cp /etc/sysconfig/puppet /etc/sysconfig/puppet.orig
[root@node1 ~]# vi /etc/sysconfig/puppet
[root@node1 ~]# diff -uNr \
 /etc/sysconfig/puppet.orig /etc/sysconfig/puppet
--- /etc/sysconfig/puppet.orig
+++ /etc/sysconfig/puppet
@@ -1,5 +1,5 @@
 # The puppetmaster server
-#PUPPET_SERVER=puppet
+PUPPET_SERVER=master.vnat

 # If you wish to specify the port to connect to do so here
 #PUPPET_PORT=8140

puppet から puppet master への設定変更の確認インターバルが、デフォルトでは 30 分と長いです (実運用としてはむしろ短いのですが)。テスト用に、puppet master への変更が即座に反映されるよう、5 秒を指定します。こちらの設定ファイルは、puppet のプログラム自身が読む方ですので、/etc/puppet/ 以下に入ります。設定ファイルの "main" セクション内に、"runinterval" パラメータを追加します。

[root@node1 ~]# cp \
 /etc/puppet/puppet.conf /etc/puppet/puppet.conf.orig
[root@node1 ~]# vi /etc/puppet/puppet.conf
[root@node1 ~]# diff -uNr \
 /etc/puppet/puppet.conf.orig /etc/puppet/puppet.conf
--- /etc/puppet/puppet.conf.orig
+++ /etc/puppet/puppet.conf
@@ -11,6 +11,8 @@
     # The default value is '$confdir/ssl'.
     ssldir = $vardir/ssl

+    runinterval=5
+
 [agent]
     # The file in which puppetd stores a list of the classes
     # associated with the retrieved configuratiion.  Can be loaded in

puppet サービスを起動します。

[root@node1 ~]# service puppet start

puppet master 側で、puppet からの接続を受け入れてあげます。

[root@master ~]# puppetca --list
  node1.vnat (13:C6:9C:8A:4A:5B:59:2B:EA:61:E0:87:E6:43:96:85)
[root@master ~]# puppetca --sign node1.vnat
notice: Signed certificate request for node1.vnat
notice: Removing file Puppet::SSL::CertificateRequest node1.vnat
 at '/var/lib/puppet/ssl/ca/requests/node1.vnat.pem'

動作を試してみます。デフォルトの puppet manifest ファイルに、ユーザの存在を保証させるリソースを記述します。

[root@master ~]# cat <<EOF > /etc/puppet/manifests/site.pp
user { "hoge":
  ensure     => present,
  uid        => '567',
  gid        => 'wheel',
  shell      => '/bin/sh',
  home       => '/var/hoge/',
  managehome => true,
}
EOF
[root@master ~]# service puppetmaster reload

ユーザ、追加されたかな?

[root@node1 ~]# id hoge
uid=567(hoge) gid=10(wheel) 所属グループ=10(wheel)
[root@node1 ~]# grep hoge /etc/passwd
hoge:x:567:10::/var/hoge/:/bin/sh
[root@node1 ~]# ls -ld /var/hoge/
drwx------. 4 hoge wheel 4096 11月 XX 16:23 2011 /var/hoge/

良いようですね。ではまた。

参考リンク:

# Puppet のヘンテコ設定言語を思うと、Ruby DSL を用いる Chef の方が筋が良さそうだなぁ…

2011年12月2日金曜日

Ruby の「条件式としての範囲式」の正体を探る

ご苦労様です、サイオス 那賀です。

Ruby 言語における「条件式としての範囲式」というやつが、どうも腑に落ちません (「Ruby 1.9.2 リファレンスマニュアル 演算子式 条件式としての範囲式」)。

> ["foo", "bar", "BEGIN", "hoge", "fuga", "END", "buzz"].select { |w|
    (w === "BEGIN" .. w === "END")? true: false
  }
=> ["BEGIN", "hoge", "fuga", "END"]
> 

これはオブジェクトのリテラルではなく、言語仕様としての、何かしらの特殊な「何か」ですよね? その証拠に上記でも、3 項演算子を削って、Ruby が括弧内を条件式と判別できないようにすると、オブジェクトにはなり得ません。

> ["foo", "bar", "BEGIN", "hoge", "fuga", "END", "buzz"].select { |w|
    (w === "BEGIN" .. w === "END")
  }
ArgumentError: bad value for range
> 

ということは、ここでループ中にその状態を抱えているのは Ruby のスタック上の「何か」ですよね? なまじ何でもオブジェクトな Ruby なだけに、これも一見 Range のオブジェクトに見えるんですが…。せめて、実装上の正体だけでも見ておこうと思います。

# Ruby でも、遅延評価とか名前渡しとかできるとカッコ良かったんですけどね

Yacc の入力ファイルがあると思います。

$ find . -name "*.y"
./ruby-1.8.7-p299/parse.y

ありました。該当するトークンを探します。

static int
yylex()
{
  ...
  retry:
    switch (c = nextc()) {
      ...
      case '.':
        lex_state = EXPR_BEG;
        if ((c = nextc()) == '.') {
            if ((c = nextc()) == '.') {
                return tDOT3;
            }
            pushback(c);
            return tDOT2;
        }
        ...

static struct {
    ID token;
    const char *name;
} op_tbl[] = {
    {tDOT2,     ".."},
    {tDOT3,     "..."},
    {'+',       "+"},
    ...

"tDOT2" と "tDOT3" が "..", "..." のトークンのようです。どうパースしていますかね?

arg : lhs '=' arg
    ...
    | arg tDOT2 arg
        {
            value_expr($1);
            value_expr($3);
            $$ = NEW_DOT2($1, $3);
            if (nd_type($1) == NODE_LIT && FIXNUM_P($1->nd_lit) &&
                nd_type($3) == NODE_LIT && FIXNUM_P($3->nd_lit)) {
                deferred_nodes = list_append(deferred_nodes, $$);
            }
        }
    ...

NEW_DOT2(), NEW_DOT3() とやらで、パースツリーのノードを作っています。node.h:

#define NEW_DOT2(b,e) NEW_NODE(NODE_DOT2,b,e,0)
#define NEW_DOT3(b,e) NEW_NODE(NODE_DOT3,b,e,0)

ノードの型としては "NODE_DOT2" や "NODE_DOT3" で上がって行くようです。これは Range でも条件範囲式でも同じようですので、上で何らかの置換処理をしているんでしょう。if 式での処理を見てみます。

primary : literal
        | kIF expr_value then
          compstmt
          if_tail
          kEND
            {
                $$ = NEW_IF(cond($2), $4, $5);
                fixpos($$, $2);
                if (cond_negative(&$$->nd_cond)) {
                    NODE *tmp = $$->nd_body;
                    $$->nd_body = $$->nd_else;
                    $$->nd_else = tmp;
                }
            }

式が条件式だった時には、cond() 関数 → cond0() 関数で何かの変換をしているようです。

static NODE*
cond0(node)
    NODE *node;
{
  ...
  switch (nd_type(node)) {
    ...
    case NODE_DOT2:
    case NODE_DOT3:
      node->nd_beg = range_op(node->nd_beg);
      node->nd_end = range_op(node->nd_end);
      if (nd_type(node) == NODE_DOT2) nd_set_type(node,NODE_FLIP2);
      else if (nd_type(node) == NODE_DOT3) nd_set_type(node, NODE_FLIP3);
      node->nd_cnt = local_append(internal_id());
      if (!e_option_supplied()) {
        int b = literal_node(node->nd_beg);
        int e = literal_node(node->nd_end);
        if ((b == 1 && e == 1) || (b + e >= 2 && RTEST(ruby_verbose))) {
          parser_warn(node, "range literal in condition");
        }
      }
      break;
      ...

ありました、条件式に指定された場合には、パースツリーのノード型を変えています。どうやら Ruby の「条件式としての範囲式」は、内部的には、ドットの数によってそれぞれ「flip2」「flip3」と呼ばれているようです。

となれば、処理がされる箇所を探すのは簡単です。eval.c:

static VALUE
rb_eval(self, n)
    VALUE self;
    NODE *n;
{
  ...
  switch (nd_type(node)) {
    ...
    case NODE_FLIP2:          /* like AWK */
      {
        VALUE *flip = rb_svar(node->nd_cnt);
        if (!flip) rb_bug("unexpected local variable");
        if (!RTEST(*flip)) {
          if (RTEST(rb_eval(self, node->nd_beg))) {
            *flip = RTEST(rb_eval(self, node->nd_end))?Qfalse:Qtrue;
            result = Qtrue;
          }
          else {
            result = Qfalse;
          }
        }
        else {
          if (RTEST(rb_eval(self, node->nd_end))) {
            *flip = Qfalse;
          }
          result = Qtrue;
        }
      }
      break;

なるほど、フリップフロップですか。ここでの鍵は rb_svar() でしょうか。

VALUE *
rb_svar(cnt)
    int cnt;
{
    struct RVarmap *vars = ruby_dyna_vars;
    ID id;

    if (!ruby_scope->local_tbl) return NULL;
    if (cnt >= ruby_scope->local_tbl[0]) return NULL;
    id = ruby_scope->local_tbl[cnt+1];
    while (vars) {
        if (vars->id == id) return &vars->val;
        vars = vars->next;
    }
    if (ruby_scope->local_vars == 0) return NULL;
    return &ruby_scope->local_vars[cnt];
}

Ruby のスコープのローカルで、対応する変数の入れモノを返してくれるようです。PUSH_SCOPE() を見ると、ruby_scope はスタックを push するたびに作られているようですので、スレッドごとに独立するんでしょう。

#define PUSH_SCOPE() do {               \
    volatile int _vmode = scope_vmode;  \
    struct SCOPE * volatile _old;       \
    NEWOBJ(_scope, struct SCOPE);       \
    OBJSETUP(_scope, 0, T_SCOPE);       \
    _scope->local_tbl = 0;              \
    _scope->local_vars = 0;             \
    _scope->flags = 0;                  \
    _old = ruby_scope;                  \
    ruby_scope = _scope;                \
    scope_vmode = SCOPE_PUBLIC

というわけで、「条件式としての範囲式」は、内部的には Range 型オブジェクトなどではなく、「flip2」「flip3」と呼ばれる言語仕様であり、状態はローカルのスコープごとに持っているようです。

注意: このアプローチには時に、言語設計者がせっかく隠蔽してくれているものを、実装レベルで見て分かったような気になってしまう罠が隠れています。例えば、Scala の吐いた class ファイルの逆コンパイルをしたところで、実装の理解には役立っても、言語の理解には全く役立たないどころか、むしろ有害だったりします。

では。

2011年12月1日木曜日

PostgreSQL の無限再帰 WITH 句で思うこと

お世話になっております、サイオス 那賀です。

PostgreSQL の再帰 WITH 句で、無限長のフィボナッチ数列を作ってみます。

template1=# WITH RECURSIVE
              f(a, b) AS (VALUES(0, 1) UNION SELECT b, a + b FROM f)
            SELECT a FROM f OFFSET 0 LIMIT 20;
  a
------
    0
    1
    1
    2
    3
    5
    8
   13
   21
   34
   55
   89
  144
  233
  377
  610
  987
 1597
 2584
 4181
(20 rows)

template1=# 

この "LIMIT" の用法は、下記の通り可搬性が低いので推奨はできないようですが、ちょっと面白いので。

これが動作するのは、PostgreSQLの実装が、実際に親問い合わせで取り出されるのと同じ数のWITH問い合わせの行のみを評価するからです。 この秘訣を実稼動環境で使用することは勧められません。 他のシステムでは異なった動作をする可能性があるからです。(「WITH問い合わせ(共通テーブル式)」 - PostgreSQL 文書

「必要な部分しか持ってこないので、データ元が無限でも OK」というところが、ちょっと Haskell 等の遅延評価型言語における無限長リストを思い起こさせます。

無限長のフィボナッチ数列。

module Fib where
  f = 0:1:zipWith (+) f (tail f)

その頭 20 個を取得。

*Fib> take 20 f
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181]
*Fib>

RDBMS で、複雑な結合の実行計画を賢く最適化して、実際に必要なデータだけをストレージからフェッチしてくれる機能というのは、どこか遅延評価の考え方に似ているように思われます。

CREATE TABLE foo (k integer PRIMARY KEY, v text);
TRUNCATE TABLE foo;
INSERT INTO foo VALUES(1, '1st foo');
INSERT INTO foo VALUES(2, '2nd foo');
INSERT INTO foo VALUES(3, '3rd foo');
INSERT INTO foo VALUES(5, '5th foo');
INSERT INTO foo VALUES(7, '7th foo');

CREATE TABLE bar (k integer PRIMARY KEY, v text);
TRUNCATE TABLE bar;
INSERT INTO bar VALUES(1, '1st bar');
INSERT INTO bar VALUES(2, '2nd bar');
INSERT INTO bar VALUES(4, '4th bar');
INSERT INTO bar VALUES(6, '6th bar');
INSERT INTO bar VALUES(8, '8th bar');

当たり前ですが、直積をとってから絞り込むような、手続き型処理的なアホなことはしていませんね。

template1=# EXPLAIN SELECT * FROM foo CROSS JOIN bar;
                             QUERY PLAN
--------------------------------------------------------------------
 Nested Loop  (cost=23.53..30303.83 rows=1512900 width=72)
   ->  Seq Scan on foo  (cost=0.00..22.30 rows=1230 width=36)
   ->  Materialize  (cost=23.53..35.83 rows=1230 width=36)
         ->  Seq Scan on bar  (cost=0.00..22.30 rows=1230 width=36)
(4 rows)

template1=# EXPLAIN SELECT * FROM foo CROSS JOIN bar WHERE foo.k = bar.k;
                             QUERY PLAN
--------------------------------------------------------------------
 Merge Join  (cost=170.85..290.46 rows=7564 width=72)
   Merge Cond: (foo.k = bar.k)
   ->  Sort  (cost=85.43..88.50 rows=1230 width=36)
         Sort Key: foo.k
         ->  Seq Scan on foo  (cost=0.00..22.30 rows=1230 width=36)
   ->  Sort  (cost=85.43..88.50 rows=1230 width=36)
         Sort Key: bar.k
         ->  Seq Scan on bar  (cost=0.00..22.30 rows=1230 width=36)
(8 rows)

template1=#

PostgreSQL、賢いですねぇ。実行計画の最適化こそがデータベースの真髄ですな。

ではまた。