そろそろLDAPにしてみないか?

第6回OpenSSHの公開鍵をLDAPで管理

公開鍵管理の概要

読者の皆さんの多くはリモートメンテナンスのために、各サーバでsshデーモンを動作させているはずです。しかしtelnetではなくsshにすればそれだけで安心安全、というわけではありません。共通鍵認証ではそれぞれの通信自体は暗号化されているとはいえ、近年では総当たり攻撃のターゲットとなっているケースも非常に多くセキュリティ的に安心できるものではないためです。皆さんはちゃんとRSAやDSAによる公開鍵認証を利用されていますか?

公開鍵認証のメリットは、共通鍵認証と比較して、より安全な認証を実現することができる点にあります。その一方、クライアント側には秘密鍵ファイルと多くの場合はパスフレーズが、サーバ側には公開鍵ファイルが必要になるため、デメリットとしてユーザ数が多いとそれらの管理も煩雑になることが挙げられます。

たとえば管理対象のサーバが100台あるとすれば、あるユーザの入社時、退職時には100台のサーバの公開鍵情報を追加、削除しなければなりません。当然これらの作業はシェルスクリプトなどで半自動化されているのが望ましいのですが、LDAPサーバを有効活用することで、さらにその手間を軽減させることができます。公開鍵情報をそのままLDAP上に登録しておき、sshdプロセスがLDAP上の公開鍵情報を参照してくれれば良いのです。この方式であれば、入社時にはLDAPサーバに対して鍵をldapadd退職時には同様にldapdeleteするだけで非常に作業が楽になります。

OpenSSHのインストール

sshデーモンと聞いて皆さんが真っ先に思いつくのはOpenSSHでしょう。もともとOpenBSD向けのソフトウェアでしたが、さまざまなプラットフォームに移植されており、SSHサーバを実現するためのデファクトスタンダードであると言って過言ではありません。

本稿執筆時のOpenSSHの最新バージョンは4.7p1です。バージョンが上がるにつれ、さまざまな機能が追加されているのですが、ソース中からgrepしてみればわかるとおり、最新版も実はそれだけでLDAP認証に対応しているわけではありません。

公開鍵をLDAP上に格納し、それをsshdプロセスから参照させるためにはパッチが必要となります。http://dev.inversepath.com/trac/openssh-lpkより最新のパッチをダウンロードしてください。執筆時の最新バージョンはopenssh-lpk-4.6p1-0.3.9.patchでしたが、4.7p1にも問題なく適用することができました図1⁠。

図1 パッチの適用とOpenSSHのインストール
% tar zxfv openssh-4.7p1.tar.gz
% cd openssh-4.7p1
% patch -p2 < ../openssh-lpk-4.6p1-0.3.9.patch
% ./configure --with-ldap && make
# make install

今回は便宜上、--with-ldapオプションのみ利用していますので、必要に応じてその他のオプションを有効にしてください。インストール後、lddコマンドでバイナリのライブラリ依存を確認し、LDAPライブラリがうまくリンクされていれば成功です。パッチが当たっているかはデフォルトの設定ファイルからも確認することもできます。

図2 lddコマンドによるライブラリの確認
% ldd /usr/local/sbin/sshd  | egrep '(ldap|lber)'
        libldap-2.2.so.7 => /usr/lib/libldap-2.2.so.7 (0x00a8d000)
        liblber-2.2.so.7 => /usr/lib/liblber-2.2.so.7 (0x0035c000)
図3 設定ファイルの確認
% grep -i ldap /usr/local/etc/sshd_config

なお、ディストリビューションによってはLDAP対応OpenSSHが配布されている場合があります。たとえばGentoo Linuxの場合、

# USE=LDAP emerge openssh

とすればLDAP対応版のOpenSSHがインストールできますし、Mandriva Linuxの場合は

# rpmbuild --rebuild openssh-4.3p2-12mdv2007.0.src.rpm --with ldap

のような形でバイナリパッケージを作成することができます。詳細については上記パッチ配布サイトのドキュメントを参照してみてください。後々のメンテナンスを考えるとパッケージからの導入をお勧めします。

スキーマ設定

今回の目標は、ユーザの公開鍵情報をLDAP上に格納し、sshdプロセスにそれを参照させることです。

過去の記事でも述べましたように、LDAP上に何らかの値を登録する際には、そのデータ用のスキーマという箱のようなものが必要になります。userPasswordやtelephoneNumberなどの属性は多くのソフトウェアから参照されるため、標準のスキーマに含まれているのですが、OpenLDAPの公開鍵用のスキーマは一般的なものではありませんので、ユーザ自身で準備する必要があります。

しかし、ゼロから作成する必要はありません。先ほどのサイトでパッチとともにスキーマファイルが配布されているので、ありがたくそのまま利用させて頂くことにしましょう。

http://dev.inversepath.com/trac/openssh-lpkよりopenssh-lpk_openldap.schemaをダウンロードし、/etc/openldap/schema以下に保存しておきます。/etc/openldap/slapd.confでは、このファイルを参照できるようリスト1の構文を追加しておき、slapdプロセスを再起動させておきましょう。

リスト1 /etc/openldap/slapd.confに追加する1行
include /etc/openldap/schema/openssh-lpk_openldap.schema

なお、LDAPスキーマに関しては、日本LDAPユーザ会の太田さんがわかりやすい資料を作成されています。LDAPユーザ会メーリングリストの投稿数はまだそれほど多くないのですが、まだの方はぜひメーリングリストにも参加してみてください。

ではデータを登録してみます。普段ssh接続に使用している公開鍵ファイル、つまりサーバ側で通常authorized_keys2という名前で保存されているファイルを用意してください。通常であれば皆さんはクライアント側の~/.ssh/id_rsa.pubまたは~/.ssh/id_dsa.pubというファイルを公開鍵として使用されているはずです。公開鍵認証を使用されていない読者の方もいらっしゃるでしょうから、今回は公開鍵の作成から行っていくこととしましょう。

公開鍵、秘密鍵の作成

次のように、秘密鍵と公開鍵を作成します。今回はDSA形式を選択しています。

図4 公開鍵、秘密鍵の作成
% ssh-keygen -t dsa
Generating public/private dsa key pair. 
Enter file in which to save the key (/home/nomo/.ssh/id_dsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/nomo/.ssh/id_dsa. 
Your public key has been saved in /home/nomo/.ssh/id_dsa.pub. 
The key fingerprint is: 
c3:ca:8b:58:8b:1a:6c:84:8b:8b:bd:06:e1:1e:20:4f nomo@rx8 

この操作で作成される~/.ssh/id_dsaが秘密鍵、~/.ssh/id_dsa.pubが公開鍵です。秘密鍵は途中で入力されるパスフレーズにより暗号化された状態で保存されます。後者の公開鍵は文字通り公開するものなので、他人に見せても問題ありません。

次にLDAPサーバ側での操作です。本連載の第2回でUNIXアカウントをLDAPサーバ側に登録しました。同じユーザツリーの中に公開鍵情報を登録してみましょう。

次のように、公開鍵用の属性とオブジェクトクラスを追加します。変更部分はldapPublicKeyというオブジェクトクラスとsshPublicKeyという公開鍵用の属性です。この中にはid_rsa.pubやauthorized_keys2の中に1行で記録されている鍵情報をそのまま登録します。

リスト2 ユーザ用エントリ(sshuser.ldif)
dn: uid=sshuser,ou=People,dc=example,dc=com
objectClass: account
objectClass: posixAccount
objectClass: ldapPublicKey
uid: sshuser
cn: sshuser
userPassword: sshuser
loginShell: /bin/bash
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/sshuser
sshPublicKey: ssh-dss AAAAB3NzaC1kc3MAAACBAJ5sBOzM/PCkETefX7yzrs+oEVOp3hwuBSpL96pbkfoyQ5jvMABT6aGzXqQUTZu00Gke+G+CJeOg3rw9K7+ghrNuB4Rv33l1LdILjTTMFqDsvMo02Un6DKv/EvAW++rarKDDU5DMJOEPqkOWTsPb683WP77fHcBxKsALVtVAFTMzAAAAFQDVPmzJd39IYvKMWQsJQvzPefUX8wAAAIEAliBMDP2SFtPoAZMAbCtAwWtQmXh7C/+CJwrQEJMDYb1Pp+7jaOk+7AgsGMTA2abtSsPDvvhlrNXOyqx+EMYxibwnX4dnGS7NQAsQhqUmvqzzKfySD/UvJ6GQYtB9FMpju0L/qH5B5jtdfwggXTaGRXuadnzAZ7rrOOMvosqyhc8AAACBAInNQo10pbrnkp9grL+Db2/Rp1JXVajN02isPzfpS7uX9rohAlyTVLAjlwLwTGrp6CFwG4/t9e7jxlIo4Wm2r7LXgLr9u7+dg+oMENJpYkt/0NtLBq40dICE8yhha58cQau5z98Ajc6dO9yvB2Bp6C3oDIiumPq/e2IMGjYYrtcP nomo@rx8

ではこのエントリをLDAP上に登録します。

図5 変更部分のLDAPへの反映
% ldapadd -x -D "cn=Manager,dc=example,dc=com" -w secret -f sshuser.ldif

次にsshdプロセスがLDAPサーバを検索できるよう、/usr/local/etc/sshd_configを修正しておきます。リスト3のようにLDAP関連のオプションを有効にしておきましょう。

リスト3 sshd_configの修正部分
UseLPK yes
LpkServers  ldap://10.0.100.10
LpkUserDN   ou=people,dc=example,dc=com
LpkGroupDN  ou=group,dc=example,dc=com

準備ができたら、sshdプロセスを起動させ、クライアントから接続を行ってみます。

図5 sshdプロセスの起動
# /usr/local/sbin/sshd
図6 クライアントからの接続試験
% ssh sshuser@10.0.100.11
Enter passphrase for key '/home/nomo/.ssh/id_dsa': 
Last login: Fri Nov  9 23:30:14 2007 from 10.0.100.1
-bash-3.00$

このように、サーバ側にauthorized_keys2ファイルを置いていない状態でもうまく認証、接続できれば成功です。正常な場合の検索プロセスは次のようになります。

  1. sshサーバがユーザ名(sshuser)を受け取る
  2. LpkUserDNやLpkGroupDN以下からsshuserのエントリを検索
  3. もし見つかれば、sshPublicKey属性を取得し、その値を公開鍵として扱う
  4. あとは通常通り認証

うまくいかない場合はLDAPサーバのログと照らし合わせたり、sshd_configsyslog関連の設定を変更して試してみてください。その際のログはリスト4のようになります。filter="(&(objectClass=posixAccount)(objectClass=ldapPublicKey)(uid=sshuser))"という検索フィルタによって公開鍵情報が読み込まれていることがわかるはずです。

リスト4 LDAPによる公開鍵認証が成功した場合のログ
Nov 10 02:40:53 localhost slapd[31307]: conn=2 fd=10 ACCEPT from IP=10.0.100.11:32831 (IP=0.0.0.0:389) 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=0 BIND dn="" method=128 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=0 RESULT tag=97 err=0 text= 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 fd=11 ACCEPT from IP=10.0.100.11:32832 (IP=0.0.0.0:389) 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 op=0 BIND dn="" method=128 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 op=0 RESULT tag=97 err=0 text= 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 op=1 SRCH base="dc=example,dc=com" scope=2 deref=0 filter="(&(objectClass=shadowAccount)(uid=sshuser))" 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 op=1 SRCH attr=uid userPassword shadowLastChange shadowMax shadowMin shadowWarning shadowInactive shadowExpire shadowFlag 
Nov 10 02:40:53 localhost slapd[31307]: conn=3 op=1 SEARCH RESULT tag=101 err=0 nentries=0 text= 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=1 SRCH base="ou=people,dc=example,dc=com" scope=2 deref=0 filter="(&(objectClass=posixAccount)(objectClass=ldapPublicKey)(uid=sshuser))" 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=1 SRCH attr=sshPublicKey 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=1 SEARCH RESULT tag=101 err=0 nentries=1 text= 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=2 SRCH base="ou=people,dc=example,dc=com" scope=2 deref=0 filter="(&(objectClass=posixAccount)(objectClass=ldapPublicKey)(uid=sshuser))" 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=2 SRCH attr=sshPublicKey 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=2 SEARCH RESULT tag=101 err=0 nentries=1 text= 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=3 SRCH base="ou=people,dc=example,dc=com" scope=2 deref=0 filter="(&(objectClass=posixAccount)(objectClass=ldapPublicKey)(uid=sshuser))" 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=3 SRCH attr=sshPublicKey 
Nov 10 02:40:53 localhost slapd[31307]: conn=2 op=3 SEARCH RESULT tag=101 err=0 nentries=1 text= 
Nov 10 02:40:58 localhost slapd[31307]: conn=2 op=4 SRCH base="ou=people,dc=example,dc=com" scope=2 deref=0 filter="(&(objectClass=posixAccount)(objectClass=ldapPublicKey)(uid=sshuser))" 
Nov 10 02:40:58 localhost slapd[31307]: conn=2 op=4 SRCH attr=sshPublicKey 
Nov 10 02:40:58 localhost slapd[31307]: conn=2 op=4 SEARCH RESULT tag=101 err=0 nentries=1 text= 
Nov 10 02:40:59 localhost slapd[31307]: conn=2 fd=10 closed 
Nov 10 02:40:59 localhost slapd[31307]: conn=3 fd=11 closed 

メンテナンス

公開鍵情報を1か所で管理できるのが今回の一番のメリットですが、SSHサーバとLDAPサーバ間のネットワークに障害が発生してしまった場合、公開鍵を参照できず、すべてのユーザがサーバにログインできない、という事態も考えられます。これに関しては途中の経路を冗長化させるなり、LDAPサーバをHAクラスタで運用することで回避できるのですが、定期的にLDAPサーバに接続し、公開鍵情報をファイルとしてダウンロードさせておくのもひとつの手でしょう。

ページの都合上、エラー処理などとくに行っておらず恐縮ですが、リスト5のスクリプトを参考にしてください。このスクリプトはNet::LDAPモジュールを利用して、authorized_keys2ファイルを自動的に作成します。

動作確認ができればcrontabなどに登録し、定期的にスクリプトを走らせるようにしてください。

リスト5 公開鍵ファイルを定期アップデートするためのスクリプト
#!/usr/bin/perl
use Net::LDAP;

$ldap = Net::LDAP->new ("localhost");
$rs = $ldap->search (base   => "dc=example,dc=com",
                     filter => "(&(homeDirectory=*)(sshPublicKey=*))");

@entries = $rs->entries;
foreach $entr ( @entries ) {
    $pubkey = $entr->get_value("sshPublicKey");
    $home = $entr->get_value("homeDirectory");
    $keyfile = "$home/.ssh/authorized_keys2";
    if ( -f $keyfile ) {
        print "updating $keyfile\n";
        open(OUT, ">$keyfile");
        print OUT "$pubkey\n";
        close(OUT);
    }
}

まとめ

今回のポイントは以下の通りです。

  • OpenSSHにLDAP認証用のパッチを当てる必要がある
  • 公開鍵用の情報は標準スキーマに登録されていないため独自で定義する

サーバ数が1台や2台など小規模な環境の場合、従来より構成が複雑になってしまい、かえってトラブルを招くこともあるかもしれませんが、大規模な環境には効果抜群です。

スキーマの追加方法などもわかりましたので、これからはさらにさまざまなソフトウェアをLDAP対応させることができますね!

おすすめ記事

記事・ニュース一覧