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、賢いですねぇ。実行計画の最適化こそがデータベースの真髄ですな。

ではまた。

2011年11月30日水曜日

RHEL6 系で qemu-kvm バイナリを CPU エミュレーション・モードで使う

お疲れ様です、サイオス 那賀です。突然ですが、今どきの仮想マシンにの実現方法としては、おおざっぱに以下の 3 種類があります。

CPU エミュレーション (インタプリタや、全部バイナリ変換 (Binary Translation))
QEMU, Bochs など。超重いが、他 CPU の命令実行もできて柔軟。実装が大変
トラップ実行 (Trap and Execute。TAE とでも?) + 部分バイナリ変換
(Workstation など、昔ながらの) VMware (実は ESXi でもできるんですが), (昔ながらの) VirtualBox, KQEMU (きわめて残念なことに開発終了) など。ハードウェア支援が不要。CPU 命令をネイティブで走らせるので、そこそこ速い。実装が大変
ハードウェア支援仮想化 (Hardware-Assisted Virtualization, HAV)
QEMU virtualizer (QEMU-KVM), Xen-HVM, HAV モードの VMware (ESXi とか), HAV モードの VirtualBox など。intel VT-xAMD-V が必要。速い、と言われている。実装が(比較的に)簡単

# 何かと面倒な準仮想化 (Para-virtualization, PV) や、OS 仮想化のコンテナ方式については脇に置きます

一般に、前者ほど遅くて後者ほど速いです(初期の VT-x は root, non-root 移行のコストが高くて、むしろ TAE の方が速かったりもしたらしいですが)。最近の実用環境では HAV が主流なのですが、HAV 方式の欠点として、VM 環境上では VM が動かせないことがあります。

# AMD-V だと、VM のスタックができるというウワサを聞きましたが、よく知りません →「Running OpenStack under VirtualBox « System Administration and Architecture Blog

何がしたいかと言うと、EucalyptusOpenStack のような IaaS プラットフォームをお手軽にテストしたいのですが、実マシンをそうそうゴロゴロと用意できない時に、VM 上でテストをできるようにしたいのです。しかし、それらのプロットフォームは、最近の流行からして、開発のプライマリに据えているハイパーバイザが KVM なのです。

これらは、実装的には libvirt 配下で VM ハイパーバイザを動かすようになっているので、理屈上は Xen や VirtualBox でも動きそうなものですが、KVM が開発のプライマリなので、何かと動きません。そもそも、RHEL6 だと Xen の Dom0 も動きませんし、RHEL5 だと Python その他が古すぎて IaaS プラットフォームが動かなかったりもします。

そこで、KVM も動作には QEMU-KVM を使っているので、かわりに QEMU を使うと結構動きます。しかし残念なことに、以前はあった KQEMU による TAE 方式は開発が止まってしまったので、泣く泣く QEMU の CPU エミュレーションモードを使います。

RHEL6 に入っている "qemu-kvm" というコマンドは、実態としては "qemu-system-x86_64" であり、ビルド時に名前を変えているだけです。このバイナリは、KVM のオプションをつければ KVM を利用した HAV 方式で動きますが、KVM を切っておけば、CPU エミュレーションでも動きます。KVM virtualizer 推しのために、emulator をわざと使えなくしているようです。

libvirt-0.8.7/src/qemu/qemu_capabilities.c:

static int
qemuCapsInitGuest(virCapsPtr caps,
                  virCapsPtr old_caps,
                  const char *hostmachine,
                  const struct qemu_arch_info *info,
                  int hvm)
{
  ...
  if (STREQ(info->arch, hostmachine) ||
   (STREQ(hostmachine, "x86_64") && STREQ(info->arch, "i686"))) {
    if (access("/dev/kvm", F_OK) == 0) {
      ...
      const char *const kvmbins[] =
       { "/usr/libexec/qemu-kvm", /* RHEL */
         "qemu-kvm", /* Fedora */
         "kvm" }; /* Upstream .spec */
      ...
    }
    if (access("/dev/kqemu", F_OK) == 0)
      haskqemu = 1;
  }

上記のように、libvirt は、コマンドのパスとモジュールのロード状況に基づいて、その環境上で利用可能なハイパーバイザーを判断しています。よって、RHEL6 でもリンクを張るだけで、libvirt 経由で QEMU のエミュレーションモードが使えるようになります。

# ln -sf /usr/libexec/qemu-kvm /usr/bin/qemu-system-x86_64

これで、VM 上で QEMU の完全仮想化エミュレーションが動くようになります。Virt-Manager 上での表示としては、以下のようになります。

「Hypervisor」「Architecture」
KVM 有効の virtualizer「KVM」「x86_64」
QEMU emulator「qemu」「x86_64」

以下は、Windows 版の VMware Workstation 上の Scientific Linux 6 (x64) 上の Virt-Manager 上で Scientific Linux (IA32) を動かした例です。

もちろん、とっても遅いです。しかし、動かないよりはマシですので、テスト用に使っています。

では。

2011年11月29日火曜日

なぜオンライン EXT3 の LVM2 スナップショット & バックアップは問題なく動くのか?

お疲れ様です、サイオス 那賀です。新書本風のタイトルにしてみました。

最初に、私の自宅サーバの、えらく大雑把なバックアップを紹介しようと思います。日に一回、ルート直下のスナップショットを、まるごと別のハードディスクに rsync で増分バックアップしています。システムは以下のようにマウントされています。/ 直下が LVM であり、EXT4 でマウントされています。

$ mount | grep /dev/mapper
/dev/mapper/vg_main-lv_main on / type ext4 (rw,errors=remount-ro)
/dev/mapper/vg_sub-lv_sub on /sub type ext4 (rw)

以下は、1 日 1 回実行されるバックアップスクリプトです。LVM スナップショットをリードオンリーでマウントし、rsync でコピーするだけの簡単なものです。

$ cat /etc/cron.daily/backup-snapshot
#!/bin/sh

mntpnt=/mnt/tmp

# /vm/ は大きいので、日曜日にだけバックアップ
foptvm=
# From Sunday, 0-indexed
test $(date +%w) != 0 && foptvm="$foptvm --exclude /vm/"

mkdir -p /sub/_
lvcreate --size 30G --snapshot --name lv_snap /dev/vg_main/lv_main
mount -o ro /dev/vg_main/lv_snap $mntpnt
if test -d $mntpnt/proc
then
  rsync -avr --delete $foptvm $mntpnt/ /sub/_
fi
umount $mntpnt
lvremove -f /dev/vg_main/lv_snap

当初は、DB ごとに個人データごとにと、個別にバックアップをとっていたのですが、サーバの用途が増えてきたためだんだん面倒くさくなってきてしまい、このような丸ごとバックアップに落ち着きました。問題なく動いていたので、これで正しいのだろうと思っていたのですが、ふとログを見ると、以下のようなメッセージが出ていました。

$ grep EXT4 /var/log/messages
Nov XX 06:25:03 queen kernel: [10216804.075357] EXT4-fs (dm-3): orphan cleanup on readonly fs
Nov XX 06:25:03 queen kernel: [10216804.151685] EXT4-fs (dm-3): 43 orphan inodes deleted
Nov XX 06:25:03 queen kernel: [10216804.151689] EXT4-fs (dm-3): recovery complete
Nov XX 06:25:04 queen kernel: [10216804.249053] EXT4-fs (dm-3): mounted filesystem with ordered data mode. Opts: (null)
Nov YY 06:25:03 queen kernel: [10303060.217676] EXT4-fs (dm-3): orphan cleanup on readonly fs
Nov YY 06:25:03 queen kernel: [10303060.542436] EXT4-fs (dm-3): 45 orphan inodes deleted
Nov YY 06:25:03 queen kernel: [10303060.542441] EXT4-fs (dm-3): recovery complete
Nov YY 06:25:03 queen kernel: [10303060.648882] EXT4-fs (dm-3): mounted filesystem with ordered data mode. Opts: (null)
Nov ZZ 06:25:02 queen kernel: [10389317.017146] EXT4-fs (dm-3): orphan cleanup on readonly fs
Nov ZZ 06:25:02 queen kernel: [10389317.117018] EXT4-fs (dm-3): 45 orphan inodes deleted
Nov ZZ 06:25:02 queen kernel: [10389317.117023] EXT4-fs (dm-3): recovery complete
Nov ZZ 06:25:03 queen kernel: [10389317.215482] EXT4-fs (dm-3): mounted filesystem with ordered data mode. Opts: (null)

特に気になるのが「~ orphan inodes deleted」です。リードオンリーでマウントしているはずなのに、EXT4 のクリーンアップが動いて、ファイルシステムに変更を加えているように見えます。この分だと、ジャーナルのリプレイもしているのではないでしょうか? むしろジャーナルのリプレイができないとなると、マウントしたままの EXT4 のブロックデバイスイメージから取っているスナップショットですので、ファイルシステムには不整合が生じているのでは? そもそも、LVM のスナップショットって書きこめるの?

と、疑問が出てきてしまったので、検証してみました。まずは、「LVM のスナップショットに書き込めるのか?」から。

root@queen:~# lvcreate --size 30G --snapshot --name lv_snap \
 /dev/vg_main/lv_main
  Logical volume "lv_snap" created
root@queen:~# mount -o rw /dev/vg_main/lv_main /mnt/tmp
root@queen:~# ls -l /mnt/tmp/hoge
ls: cannot access /mnt/tmp/hoge: No such file or directory
root@queen:~# echo hoge > /mnt/tmp/hoge
root@queen:~# cat /mnt/tmp/hoge
hoge
root@queen:~# umount /mnt/tmp/
root@queen:~# lvremove -f /dev/vg_main/lv_snap
  Logical volume "lv_snap" successfully removed

余裕で書き込めますね。知らなかった…。「LVM HOWTO」の「Snapshots」の項に、以下のようにあります。

In LVM2, snapshots are read/write by default. Read/write snapshots work like read-only snapshots, with the additional feature that if data is written to the snapshot, that block is marked in the exception table as used, and never gets copied from the original volume.

LVM2 になってからは、スナップショットの差分領域は、スナップショット元からの差分を保持すると同時に、スナップショットへの書き込み差分も保持するようになっているそうです。静的なスナップショットというよりも、動的に書き込める、揮発的なブランチのようなものですね。

さて、LVM スナップショットに書き込めることは分かったので、次は、リードオンリー・マウントでも EXT4 がマウント時にファイルシステムの修復を行うかどうか、です。

余談なのですが、最近の Linux は I/O フリーズの機能を持っています。ファイルシステムを一貫性のある状態に移行させてから一時「凍結」し、バックアップやコピーをとるための機能です。これを使えば、以下のようにきれいなスナップショットがとれます。

  • ioctl(2) に FIFREEZE で、I/O を止め、一時的にファイルシステムを一貫性のある状態に保つ
  • LVM でブロックデバイスのスナップショットをとる
  • FITHAW で、フリーズを解除
  • スナップショットをマウントし、ゆっくりと rsync ででもコピーをとる
  • アンマウントして、スナップショットを開放

しかし残念ながら、今の LVM などでは、この機能を使っていません…か? FIFREEZE 等の呼び出しがないことから、てっきり使っていないと思っていたのですが、RHEL6 などで FIFREEZE/FITHAW の ioctl() を発行するためのコマンド fsfreeze(8) の man を参照すると、以下のように書かれています。

fsfreeze is unnecessary for device-mapper devices. The device-mapper (and LVM) automatically freezes filesystem on the device when a snap-shot creation is requested. For more details see the dmsetup(8) manpage.

LVM2 のコードを見てみると(かなり読みづらいコードなのですが)、lvcreate(8) でスナップショットを作成すると、device mapper の「ブロックデバイスに対して」、DM_DEV_SUSPEND_CMD で ioctl() を呼んでいます (これは、drivers/md/dm-ioctl.c まわりのコードです。対して、ファイルシステムへのコードは linux/fs/ioctl.c:ioctl_fsfreeze() にあり、FIFREEZE での ioctl() が、「ファイルシステム上のノードに対して」発行されます)。とはいえ、結果的には、両者とも同一のスーパーブロックに対して freeze_bdev() を実行することになります。ブロックデバイスのくせして、自分の上に載っているファイルシステムにまで干渉するとは、何だか妙な感じがしますが、理にかなってはいます。

よって、LVM2 でスナップショットを取る前には、一瞬 I/O を停止して、ファイルシステムを整合性を保った状態まで持って行ってからスナップショットを作成しています。

以上でスナップショットについては解決したのですが、では、異常終了時の EXT3 はどうなんでしょう。結論から言うと、「[RFC, PATCH 0/6] ext3: do not modify data on-disk when mounting read-o」や「'Add a norecovery option to ext3/4?' - MARC」を見ると分かるように、実は「-o ro」オプションをつけてマウントしても、EXT3/EXT4 は、可能であれば(デバイスが書き込み可能であれば)ジャーナルのリプレイと orphan inode の切り落としをしてしまいます。ソース中では "really_read_only" のフラグで分岐しています。

linux-2.6.32/fs/ext4/super.c:

static int ext4_load_journal(struct super_block *sb,
                             struct ext4_super_block *es,
                             unsigned long journal_devnum)
{
    ...
    int really_read_only;
    ...
    really_read_only = bdev_read_only(sb->s_bdev);
    ...
    if (EXT4_HAS_INCOMPAT_FEATURE(sb, EXT4_FEATURE_INCOMPAT_RECOVER)) {
        if (sb->s_flags & MS_RDONLY) {
            ext4_msg(sb, KERN_INFO, "INFO: recovery "
                "required on readonly filesystem");
            if (really_read_only) {
                ext4_msg(sb, KERN_ERR, "write access "
                    "unavailable, cannot proceed");
                return -EROFS;
            }
            ext4_msg(sb, KERN_INFO, "write access will "
               "be enabled during recovery");
        }
    }
    ...
}

以上をまとめますと、

  • LVM2 では、元 LV のダーティバッファはスナップショットにも反映される
  • LVM2 のスナップショットには、書き込みができる
  • スナップショットはブロックデバイスでありながら、その上層のファイルシステムと示し合わせて、一貫性のある状態でのスナップショットを取ってくれる
  • EXT3/EXT4 の "-o ro" マウントは、(journal リプレイと) orphan inode 切り落としをする

以上から、EXT3/EXT4 の、ある瞬間のスナップショットを LVM2 でとって、それをリードオンリーマウントしてコピーをとっても、一貫性の点では問題はないということですね。

やれやれ、ZFS や btrfs のような、シャドウコピーで手軽にファイルシステムレベルでスナップショットを利用できるようになれば、LVM を事前に用意する必要もないので、はるかに話は簡単になるんですが。早くデフォルトにならないかなぁ。

ではまた。

CentOS 6.0 でインストール直後にネットワークを有効にする方法

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

CentOS 6.0 の GUI インストールで、CentOS 5 系と同じように設定変更せず先に進めると、インストール後のネットワークが有効にならずに困ったことがあります。

インストールしてしまったら、/etc/sysconfig/network-scripts/ifcfg-eth0 を編集するか、GUI のメニュー 「システム」→「設定」→「ネットワーク接続」から有効化すればいいのですが、できればインストール時に有効化したいですよね?今回はその方法を紹介します。

CentOS 6.0 のインストール中にホスト名を設定する画面(画像1)が表示されたら「ネットワークの設定」ボタンをクリックします。

画像1 CentOS 6.0 インストール時ホスト名設定画面

実は、CentOS 5 系ではこのタイミングでネットワークが有効になっている(画像2)だけなんですね。

画像2 CentOS 5系のインストール時ホスト名設定画面

話を CentOS 6.0 に戻します。
次に「ネットワーク接続」のウィンドウ(画像3)の「有線」タブで「System eth0」を選択して「編集...」ボタンをクリックします。

画像3 「ネットワーク接続」ウィンドウ
「System eth0 の編集」ウィンドウ(画像4)で「自動接続する」のチェックボックスをチェックすれば、インストール後にネットワークが有効になります。

画像4 「System eth0」ウィンドウ

RHEL6 や Scientific Linux 6 でも同じ方法で変更できます。

2011年11月28日月曜日

PostgreSQL 死活監視のタイムアウト

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

スクリプト等から PostgreSQL のプロセスが正しく動作しているかどうかを監視するための方法としては、pg_ctl コマンドを使ってのチェックと、実際にポートにアクセスしてのチェックがあると思います。

まずはプロセスの確認です。pg_ctl コマンドの "status" サブコマンドで、ローカルで動作している PostgreSQL のプロセスの状態を取ることができます。

$ /usr/local/pgsql/bin/pg_ctl -D ~/pgdata-main/ status
pg_ctl: postmaster is running (PID: 10532)
Command line was:
/usr/lib/postgresql/8.4/bin/postgres "-D" "/home/knaka/pgdata-main"

"-D" オプションで指定したデータディレクトリ下には "postmaster.pid" ファイルがあり、内容は、外部からのコネクションを受け付けている Postmaster プロセスのプロセス番号 (PID) です。この PID に対応するプロセスが動作しているどうかの結果を返します。

このコマンドがブロックしてしまうことはないのでしょうか? 少しソースも見てみましょう。Postmaster の死活を見ているのは、下記の部分です。src/bin/pg_ctl/pg_ctl.c です。

static bool
postmaster_is_alive(pid_t pid)
{
  ...
  if (kill(pid, 0) == 0)
    return true;
  ...
}

kill(2) にシグナル番号 0 を投げるというのはどういう意味でしたっけ? man には以下のようにあります。

If sig is 0, then no signal is sent, but error checking is still per-formed; this can be used to check for the existence of a process ID orprocess group ID.

単にプロセスが存在しているかどうかしか見ていませんが、これがブロックすることはなさそうなので、DB の状態に拠らず安心して呼べますね。しかしこれでは、プロセスはあるけれど実際にはサービスが行われていなかった場合を検出できません。

そこで、ネットワークのポート (TCP/IP なりドメインソケットなりの) に接続して死活監視を行います (パスワードを訊かれないよう、パスワードファイルなりサービスファイルなりは事前に設定しておいてください)。

$ psql -h localhost -p 5433 -U admin template1 -l
                          List of databases
   Name    | Owner | Encoding | Collation | Ctype | Access privileges
-----------+-------+----------+-----------+-------+-------------------
 eucdb     | admin | EUC_JP   | C         | C     |
 main      | admin | UTF8     | C         | C     |
 postgres  | knaka | UTF8     | C         | C     |
 template0 | knaka | UTF8     | C         | C     | =c/knaka
(中略)
$ echo $?
0 

# なおここで、"postgres" データベースに接続してチェックするのはやめましょう。世の中には "postgres" データベースを持たない PostgreSQL データベースがあります。"template1" が無いことはありません。

しかしながら、もしポートを開いたまま死んでいる PostgreSQL へ不用意に接続して状態を確認してしまうと、いつまでも待ってしまいます。netcat コマンドで listen ポートを開いて試してみましょう。

$ nc -l 15432
(待ち...)

別のコンソールから。

$ psql -h localhost -p 15432 -U postgres postgres
(待ち...)

はい、返ってきません。もし一定時間ごとに死活監視のコマンドを発行するようにしていたら大変です。プロセスは増え続け、いずれリソースを食い尽くします。

そんな時、たとえば SSH でリモートのコマンドを叩くような状態監視であれば、タイムアウトのパラメータを渡すことで対処します。

$ nc -l 10022

$ ssh -p 10022 localhost
これだと戻りませんが。
$ ssh -o "ConnectTimeout=5" -p 10022 localhost
(5 秒経過…)
Connection timed out during banner exchange
$ echo $?
255
$ 

"ConnectionTimeout" パラメータを渡すことで、ちゃんとエラーを返すようになりました。

一方、psql コマンドのドキュメントにはそういうオプションが見当たりません。しかし psql コマンドは、実際のデータベースへの接続には libpq ライブラリを用いており、タイムアウトの設定はそちらにあります。libpq が理解する環境変数を見てみますと、"PGCONNECT_TIMEOUT=~" というのがありますね。試してみましょう。

$ PGCONNECT_TIMEOUT=5 psql -h localhost -p 15432 -U postgres postgres
(5 秒経過…)
psql: timeout expired
$ echo $?
2

めでたくタイムアウトし、エラーのリターンコードを返すようになりました。

では

2011年11月25日金曜日

ソースの入手と再ビルド ~ Ubuntu 編

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

先日のエントリで、RPM 系ディストリビューションにおけるソースの入手とビルド方法には触れましたので、今度は簡単に、Debian 系ということで Ubuntu 上でのビルドをしてみます。なお、私も RPM を主戦場としておりまして、Debian 系の習熟度は普通のシスアド程度なのですが、やはりイザという時にソースの入手やビルド程度できねばなるまいと思いまして、あえてメモ書き程度に記しておきます。

まずは、パッケージビルドをするためのツールを含む "dpkg-dev" をインストールします。そして、後で Bash のビルドを試しますので、依存パッケージをインストールしておきます。

root@queen:~# aptitude install dpkg-dev
root@queen:~# aptitude build-dep bash

以降は一般ユーザで行います。apt-get で source を入手できます。

nonpriv@queen:~$ mkdir -p ~/deb/
nonpriv@queen:~$ cd ~/deb/
nonpriv@queen:~/deb$ apt-get source bash
(中略)
nonpriv@queen:~/deb$ ls -l
total 4016
drwxr-xr-x 3 nonpriv nonpriv    4096 2011-XX-XX 14:36 bash-4.1
-rw-r--r-- 1 nonpriv nonpriv   87125 2010-08-11 05:05 bash_4.1-2ubuntu4.diff.gz
-rw-r--r-- 1 nonpriv nonpriv    1296 2010-08-11 05:05 bash_4.1-2ubuntu4.dsc
-rw-r--r-- 1 nonpriv nonpriv 4013077 2010-01-05 04:10 bash_4.1.orig.tar.gz

RPM のビルドが手続き的なのに対して、ダウンロードしてきた "bash-4.1/" ディレクトリの中を見て分かるとおり、Debian のビルドは、元のビルドシステムを生かしつつ、そこへラッパーを被せているようなノリがあります。うまく言えないけれど、そんな感じです。

ここで deb パッケージを作成するだけであれば、下記で充分です。

nonpriv@queen:~/deb/bash-4.1$ dpkg-buildpackage

右記をひととおり読めば、誰でも deb ソースパッケージの操作、作成はできるようになると思います → 「Debian 新メンテナーガイド」。SPEC ファイル書き方講座の決定版とも言える「Maximum RPM」が 10 年以上もそのままなのに比べると、Debian プロジェクトはきちんとしているなぁと思わざるを得ません。