34
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OpenLDAP Server 日本語訳

Last updated at Posted at 2016-07-06

OpenLDAP Server の日本語訳。

Lightweight Directory Access Protocol (LDAP) は
TCP/IP 上で動作する X.500 に基づくディレクトリサービスへ
問合せと更新を行う為のプロトコルである。
最新の LDAP のバージョンは RFC4510 に基づいて定義された
LDAPv3 である。 Ubuntu では OpenLDAP として実装されている。

LDAP プロトコルは LDAP ディレクトリへ接続し、
いくつかの概念と用語を持つ:

  1. LDAP ディレクトリは階層的なデータ登録のツリーとなっており、
    Directory Information Tree (DIT) と呼ばれる。

  2. 登録されたデータは属性を持つ。

  3. 属性は種類(名前 / 説明)を持ち、一つまたは複数の値を持つ。

  4. 全ての属性は最低一つの objectClass を必ず定義しなければならない。

  5. 属性と objectClass はスキーマへ定義する。
    (objectClass は実際には特別な種類の属性として認識される)

  6. 各登録は一意の識別子を持つ:
    Distinguished Name(DN または dn)
    DN は Relative Distinguished Name (RDN) を持ち、
    RDN の後には 親の DN 登録が記述される。

  7. DN の登録は属性ではない。
    DN そのものはデータ登録の一部とはみなされない。

Note:
object, container, node とさまざま呼び名があるが、
いずれも登録を意味する技術用語である。

例えば、下記の登録は 11 の属性から構成される:

  • DN は “cn=John Doe,dc=example,dc=com”

  • RDN は “cn=John Doe”

  • 親の DN は “dc=example,dc=com”

DIT
dn: cn=John Doe,dc=example,dc=com
cn: John Doe
givenName: John
sn: Doe
telephoneNumber: +1 888 555 6789
telephoneNumber: +1 888 555 1232
mail: john@example.com
manager: cn=Larry Smith,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top

上記の登録は LDIF 形式 (LDAP Data Interchange Format) で
記述されている。DIT へ記述する情報は全て
LDIF 形式で記述しなければならない。
LDIF は RFC2849 に定義されている。

本手引では LDAP を認証に使用する方法を説明する。
属性に基づく(name:value)バックエンドである LDAP は
情報の取得を要求する接続要求の大量処理に適している。
アドレス帳、 email アドレスの一覧、メールサーバの設定を
例として掲載する。

#インストール
OpenLDAP サーバデーモンと伝統的な LDAP 管理ユーティリティを
インストールする。
OpenLDAP サーバデーモンと LDAP 管理ユーティリティは
slapd と ldap-utilsのパッケージに含まれている。

slapd をインストールすると動作設定が作成される。
slapd はユーザのデータを保存することができる
データベースインスタンスを作成する。
データベースインスタンスの接尾辞(またはベース DN)は
ローカルホストのドメイン名から決定される。
独自に設定したい場合は /etc/hosts を編集してドメイン名を書換え、
任意の接尾辞を指定する。
例えば接尾辞を dc=example,dc=com としたい場合、
/etc/hosts は下記のように記述する:

/etc/hosts
127.0.1.1       hostname.example.com hostname

ユーザはパッケージインストール後に変更を元に戻すことができる。

Note:
本手引ではデータベース接尾辞として
dc=example,dc=com を使用する。

インストールを進める:

sudo apt install slapd ldap-utils

Ubuntu 8.10 から slapd は外部ファイルへ分離した
DIT を準備することで slapd の内部で
自身を設定するように設計されている。
これはサービスを再起動せずに slapd の設定を
動的に変更することを可能にする。
設定データベースは /etc/ldap/slapd.d 配下に配置された
テキストベースの LDIF ファイルのコレクションから構成される。

この動作方式はいくつかの名前で知られている:

  • the slapd-config method
  • the RTC method (Real Time Configuration)
  • the cn=config method

伝統的な flat-file method (slapd.conf) を
使用することもできるが、推奨しない;
flat-file method は段階的に廃止されていく。

Note:
Ubuntu は現在 slapd-config method を使用して
slapd の設定を行っており、
本手引では slapd-config method に従う。

インストールの間、管理者の資格情報を定義するプロンプトが表示される。
ユーザのデータベースインスタンスの roorDN の資格情報は
LDAP に基づく。
ユーザの DN の既定値は cn=admin,dc=example,dc=com
また slapd-config データベースの管理者アカウントは既定では存在せず、
LDAP の外部から認証し接続する必要がある。詳細は後述する。

今日では古典的なスキーム(cosine, nis, inetorgperson)は
slapd に内臓されている。
スキームを動作させる為に前もって必要な “core” スキームも含まれている。

#インストール後の検証
インストール処理では 2 つの DIT が設定される。
一つは slapd-config 、
もう一つはユーザ自身のデータ(dc=example,dc=com)である。
以下に例を掲載する。

slapd-config は database/DIT の形式となる。
データベースは LDIF に基づき、
/etc/ldap/slapd.d 配下で動作する:

  • /etc/ldap/slapd.d/
  • /etc/ldap/slapd.d/cn=config
  • /etc/ldap/slapd.d/cn=config/cn=module{0}.ldif
  • /etc/ldap/slapd.d/cn=config/cn=schema
  • /etc/ldap/slapd.d/cn=config/cn=schema/cn={0}core.ldif
  • /etc/ldap/slapd.d/cn=config/cn=schema/cn={1}cosine.ldif
  • /etc/ldap/slapd.d/cn=config/cn=schema/cn={2}nis.ldif
  • /etc/ldap/slapd.d/cn=config/cn=schema/cn={3}inetorgperson.ldif
  • /etc/ldap/slapd.d/cn=config/cn=schema.ldif
  • /etc/ldap/slapd.d/cn=config/olcBackend={0}hdb.ldif
  • /etc/ldap/slapd.d/cn=config/olcDatabase={0}config.ldif
  • /etc/ldap/slapd.d/cn=config/olcDatabase={-1}frontend.ldif
  • /etc/ldap/slapd.d/cn=config/olcDatabase={1}hdb.ldif
  • /etc/ldap/slapd.d/cn=config.ldif

Note:
slapd-config データベースを直接編集してはならない。
LDAP プロトコル(ユーティリティ)経由で変更を行うこと。

LDAP プロトコル経由の slapd-config DIT の操作は下記のように行う:

Warning:
Ubuntu server 14.10 以降では
下記のコマンドはバグにより動作しないことがある。

sudo ldapsearch 
  -Q 
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config dn

dn: cn=config
dn: cn=module{0},cn=config
dn: cn=schema,cn=config
dn: cn={0}core,cn=schema,cn=config
dn: cn={1}cosine,cn=schema,cn=config
dn: cn={2}nis,cn=schema,cn=config
dn: cn={3}inetorgperson,cn=schema,cn=config
dn: olcBackend={0}hdb,cn=config
dn: olcDatabase={-1}frontend,cn=config
dn: olcDatabase={0}config,cn=config
dn: olcDatabase={1}hdb,cn=config

登録の説明:

  1. cn=config: 全体の設定

  2. cn=module{0},cn=config: 動的に読み込まれるモジュール

  3. cn=schema,cn=config: ハードコーディングされた
    システムレベルのスキームを含む

  4. cn={0}core,cn=schema,cn=config: ハードコーディングされた
    コアスキーム

  5. cn={1}cosine,cn=schema,cn=config: コサインスキーム

  6. cn={2}nis,cn=schema,cn=config: nis スキーム

  7. cn={3}inetorgperson,cn=schema,cn=config:
    inetorgperson スキーム

  8. olcBackend={0}hdb,cn=config:
    ‘hdb’ バックエンドストレージの種類

  9. olcDatabase={-1}frontend,cn=config:
    フロントエンドデータベース。他のデータベース用の既定の設定

  10. olcDatabase={0}config,cn=config:
    slapd 設定データベース (cn=config)

  11. olcDatabase={1}hdb,cn=config:
    ユーザのデータベースインスタンス (dc=examle,dc=com)

dc=example,dc=com DIT を例示する:

ldapsearch -x -LLL -H ldap:/// -b dc=example,dc=com dn

dn: dc=example,dc=com
dn: cn=admin,dc=example,dc=com

登録の説明:

  1. dc=example,dc=com: DIT のベース

  2. cn=admin,dc=example,dc=com:
    (パッケージのインストール中に設定される)
    DIT の管理者 (rootDN)

#ユーザデータベースの更新 / 移行
データベースの内容を例示する。下記を登録する:

  1. People という名前のノード(ユーザを保存する)

  2. Group という名前のノード(グループを保存する)

  3. miners という名前のグループ

  4. john という名前のユーザ

下記の LDIF ファイルを作り、
add_content.ldif と名前を付ける:

add_content.ldif
dn: ou=People,dc=example,dc=com
objectClass: organizationalUnit
ou: People

dn: ou=Groups,dc=example,dc=com
objectClass: organizationalUnit
ou: Groups

dn: cn=miners,ou=Groups,dc=example,dc=com
objectClass: posixGroup
cn: miners
gidNumber: 5000

dn: uid=john,ou=People,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: john
sn: Doe
givenName: John
cn: John Doe
displayName: John Doe
uidNumber: 10000
gidNumber: 5000
userPassword: johnldap
gecos: John Doe
loginShell: /bin/bash
homeDirectory: /home/john

Note:
ディレクトリ内で uid と gid の値が
local の値と衝突しないようにすることが重要。
5000 番から始まるような高い数値域を使用すること。
uid と gid の値を ldap 内で高く設定することによって
local ユーザが行う操作と ldap が行う操作の制御が簡単になる。
詳細については後述する。

内容を追加する。

ldapadd
  -x
  -D cn=admin,dc=example,dc=com
  -W -f add_content.ldif

Enter LDAP Password: ********
adding new entry "ou=People,dc=example,dc=com"
adding new entry "ou=Groups,dc=example,dc=com"
adding new entry "cn=miners,ou=Groups,dc=example,dc=com"
adding new entry "uid=john,ou=People,dc=example,dc=com"

ldapsearch ユーティリティを使用することで
情報が正しく登録されているかどうかを確認することができる:

ldapsearch
  -x
  -LLL
  -b dc=example,dc=com 'uid=john' cn gidNumber

dn: uid=john,ou=People,dc=example,dc=com
cn: John Doe
gidNumber: 5000

ldapsearch のスイッチの説明:

  1. -x: “simple” バインディング; 既定の SASL メソッドを使用しない

  2. -LLL: 外来情報の表示を無効にする

  3. uid=john: john という名前のユーザを検索する “filter”

  4. cn gidNumber: 特定の属性を表示するように要求する
    (既定では全ての属性が表示される)

#slapd 設定データベースの更新
slapd-config DIT は検索と更新を行うことができる。
下記に例を掲載する。

ldapmodify を使用して “Index” (DbIndex 属性) を
{1}hdb,cn=config database (dc=example,dc=com) へ追加する。
下記の内容でファイルを作成し、 uid_index.ldif と名前を付ける:

uid_index.ldif
dn: olcDatabase={1}hdb,cn=config
add: olcDbIndex
olcDbIndex: uid eq,pres,sub

下記のコマンドを実行する:

sudo ldapmodify
  -Q
  -Y EXTERNAL
  -H ldapi:///
  -f uid_index.ldif

modifying entry "olcDatabase={1}hdb,cn=config"

変更した内容は下記の方法で確認することができる:

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config '(olcDatabase={1}hdb)' olcDbIndex

dn: olcDatabase={1}hdb,cn=config
olcDbIndex: objectClass eq
olcDbIndex: uid eq,pres,sub

スキーマを追加する。スキーマの作成は LDIF 形式への変換が必要となる。
/etc/ldap/schema ディレクトリに変換前のスキームと、
変換後のスキームが配置されている。

Note:

  1. slapd-config データベースからスキームを削除すると
    重大な問題を引き起こすことがある。
    試験的なスキームの追加は試験用のシステム上で行うこと。

  2. スキームを追加する前に、
    スキームが既にインストールされているかどうかを確認すること
    (既定では out-of-the-box と出力される):

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=schema,cn=config dn

dn: cn=schema,cn=config
dn: cn={0}core,cn=schema,cn=config
dn: cn={1}cosine,cn=schema,cn=config
dn: cn={2}nis,cn=schema,cn=config
dn: cn={3}inetorgperson,cn=schema,cn=config

以下の例では CORBA スキームを登録する。

下記の内容で schema_convert.conf という名前の
変換用設定ファイルを作成する:

schema_convert.conf
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/collective.schema
include /etc/ldap/schema/corba.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/duaconf.schema
include /etc/ldap/schema/dyngroup.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/java.schema
include /etc/ldap/schema/misc.schema
include /etc/ldap/schema/nis.schema
include /etc/ldap/schema/openldap.schema
include /etc/ldap/schema/ppolicy.schema
include /etc/ldap/schema/ldapns.schema
include /etc/ldap/schema/pmi.schema

ldif_output という名前の出力用ディレクトリを作成する。

スキーマのインデックスを決定する:

slapcat -f schema_convert.conf -F ldif_output -n 0
  | grep corba,cn=schema

cn={1}corba,cn=schema,cn=config

Note:
slapd が親の DN と同じオブジェクトを受け取る場合、
受け取ったオブジェクトへインデックスを作成する。
インデックスは {X} の形式で記録される:

slapcat を使用して変換を実行する:

slapcat 
  -f schema_convert.conf
  -F ldif_output
  -n 0
  -H ldap:///cn={1}corba,cn=schema,cn=config
  -l cn=corba.ldif

変換されたスキーマは cn=corba.ldif となる。

cn=corba.ldif を編集し、下記の属性を検索する:

cn=corba.ldif
dn: cn=corba,cn=schema,cn=config
...
cn: corba

下記の行を削除する:

cn=corba.ldif
structuralObjectClass: olcSchemaConfig
entryUUID: 52109a02-66ab-1030-8be2-bbf166230478
creatorsName: cn=config
createTimestamp: 20110829165435Z
entryCSN: 20110829165435.935248Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20110829165435Z

属性の値が変更される。

最後に ldapadd を使用して
新しいスキーマを slapd-config DIT へ登録する:

sudo ldapadd
  -Q
  -Y EXTERNAL
  -H ldapi:///
  -f cn\=corba.ldif

adding new entry "cn=corba,cn=schema,cn=config"

現在読み込まれているスキーマを確認する:

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=schema,cn=config dn

dn: cn=schema,cn=config
dn: cn={0}core,cn=schema,cn=config
dn: cn={1}cosine,cn=schema,cn=config
dn: cn={2}nis,cn=schema,cn=config
dn: cn={3}inetorgperson,cn=schema,cn=config
dn: cn={4}corba,cn=schema,cn=config

Note:
外部のアプリケーションとクライアントを
LDAP を使用して認証する為には、
アプリケーションとクライアント毎に設定が必要となる。
詳細はクライアント側の文書を参照すること。

#ログの記録
OpenLDAP に基づくソリューションを実装する場合は
slapd の動作履歴の記録は欠かすことができないが、
ソフトウェアのインストール後に必ず手動で有効にしなければならない。
手動操作を行わない場合は基本的なメッセージのみがログに記録される。
他の slapd 設定と同様に、
ログの記録は slapd-config データベースを介して有効となる。

OpenLDAP は複数のログ記録サブシステム(levels)を備えており、
各 level に下位のサブシステム(additive)を保持している。
統計がその一例だ。
サブシステムの詳細については slapd-config のマニュアルページを参照。

logging.ldif という名前のファイルを下記の内容で作成する:

logging.ldif
dn: cn=config
changetype: modify
replace: olcLogLevel
olcLogLevel: stats

変更を反映する:

sudo ldapmodify
  -Q
  -Y EXTERNAL
  -H ldapi:///
  -f logging.ldif

これはログの重要度を定義し、製品として稼働を始めたシステムの
ログ詳細レベルを小さく絞る。
このモードで稼働している間、ホストの syslog エンジン(rsyslog)は
接続の維持が困難になった場合に下記のメッセージを出力することがある:

rsyslogd-2177: imuxsock lost 228 messages 
from pid 2547 due to rate-limiting

rsyslog の設定変更を検討する場合は
/etc/rsyslog.conf へ下記を追記する:

/etc/rsyslog.conf
# レートの制限を無効化する。
# (既定では 5 秒間に 200 メッセージ;
# 下記の例では 5 から 0 へ変更する)
$SystemLogRateLimitInterval 0

rsyslog デーモンを再起動する:

sudo systemctl restart syslog.service

#複製
LDAP サービスへ依存するネットワークシステムが増えるにつれて
LDAP サービスは重要さを増していく。
LDAP サービスへ依存する環境では
LDAP サーバが応答しなくなった場合の混乱を防止する為に
LDAP の冗長化(高い可用性)を行う。
冗長化は LDAP の複製を通じて行われる。

複製は Syncrepl エンジンを介してアーカイブ化される。
Syncrepl エンジンは Consumer-Provider モデルを使用して
同期を行う。本手引では下記のモードの組合せで複製機能を実装する:

  • refreshAndPersist
  • delta-syncrepl

変更が発生すると同時に Provider が
変更された登録を Consumer へ送信するが、
変更の送信のみを行い、変更の登録は行われない。

Provider の設定を行う。

LDIF ファイルを下記の内容で作成し、
provider_sync.ldif と名前を付ける:

provider_sync.ldif
# フロントエンドデータベースへインデックスを登録する。
dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: entryCSN eq
-
add: olcDbIndex
olcDbIndex: entryUUID eq

# syncprov モジュールと accesslog モジュールを読み込む。
dn: cn=module{0},cn=config
changetype: modifyadd: olcModuleLoad
olcModuleLoad: syncprov
-
add: olcModuleLoad
olcModuleLoad: accesslog

# accesslog データベースの定義
dn: olcDatabase={2}hdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcHdbConfig
olcDatabase: {2}hdb
olcDbDirectory: /var/lib/ldap/accesslog
olcSuffix: cn=accesslog
olcRootDN: cn=admin,dc=example,dc=com
olcDbIndex: default eq
olcDbIndex: entryCSN,objectClass,reqEnd,reqResult,reqStart

# Accesslog db syncprov.
dn: olcOverlay=syncprov,olcDatabase={2}hdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov
olcSpNoPresent: TRUE
olcSpReloadHint: TRUE

# プライマリデータベースの syncrepl Provider
dn: olcOverlay=syncprov,olcDatabase={1}hdb,cn=configchangetype: add
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov
olcSpNoPresent: TRUE

# プライマリデータベースの accesslog overly 定義
dn: olcOverlay=accesslog,olcDatabase={1}hdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcAccessLogConfig
olcOverlay: accesslog
olcAccessLogDB: cn=accesslog
olcAccessLogOps: writes
olcAccessLogSuccess: TRUE

# accesslog データベースを毎日スキャンし、
# 7 日前より古い登録を削除する。
olcAccessLogPurge: 07+00:00 01+00:00

管理者が保持するディレクトリに合わせて
LDIF ファイルの rootDN を変更する。

slapd の apparmor プロファイルは
accesslog データベースが
/etc/apparmor.d/local/usr.sbin.slapd
に含まれている為、調整の必要はない:

/etc/apparmor.d/local/usr.sbin.slapd
/var/lib/ldap/ r,/var/lib/ldap/** rwk,

ディレクトリを作成し、データベース設定ファイルを設置して
apparmor プロファイルを再起動する:

sudo -u openldap mkdir /var/lib/ldap/accesslog
sudo -u openldap cp /var/lib/ldap/DB_CONFIG /var/lib/ldap/accesslog
sudo systemctl reload apparmor.service

新しい内容を登録し、 apparmor へ変更を反映する為に
デーモンを再起動する:

sudo ldapadd
  -Q
  -Y EXTERNAL
  -H ldapi:///
  -f provider_sync.ldif

sudo systemctl restart slapd.service

Provider の設定は以上で完了する。

#Consumer の設定
Consumer を設定する。

手順に従ってソフトウェアをインストールする。
slapd-config データベースが
Provider と同一であることを確認しておくこと。
特にスキームとデータベースの接尾辞が同一であることを確認しておくこと。

LDIF ファイルを下記の内容で作成し、
consumer_sync.ldif と名前を付ける:

consumer_sync.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: syncprov

dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: entryUUID eq
-
add: olcSyncRepl
olcSyncRepl: rid=0
  provider=ldap://ldap01.example.com
  bindmethod=simple
  binddn="cn=admin,dc=example,dc=com"
  credentials=secret
  searchbase="dc=example,dc=com"
  logbase="cn=accesslog"
  logfilter="(&(objectClass=auditWriteObject)(reqResult=0))"
  schemachecking=on
  type=refreshAndPersist
  retry="60 +"
  syncdata=accesslog
-
add: olcUpdateRef
olcUpdateRef: ldap://ldap01.example.com

下記の属性が正しい値を確実に保持するようにする:

  1. provider (Provider サーバのホスト名または IP アドレス。
    この例では ldap01.example.com とする)

  2. binddn (管理者の DN)

  3. credentials (管理者 DN のパスワード)

  4. searchbase (データベースの接尾辞)

  5. olcUpdateRef (Provider サーバのホスト名または IP アドレス)

  6. rid (Replica ID 。複製を識別する一意の 3 桁の数値)
    各 consumer は最低でも一つの rid を保持しておく必要がある。

新しい内容を登録する:

sudo ldapadd
  -Q
  -Y EXTERNAL
  -H ldapi:///
  -f consumer_sync.ldif

Consumer の設定は以上で完了する。
2 つのデータベース(suffix: dc=example,dc=com)が
同期するようになる。

#動作テスト
複製を開始した後は Provider と Consumer の双方で
下記を実行することで監視を行うことができる。

ldapsearch
  -z1
  -LLLQY EXTERNAL
  -H ldapi:///
  -s base
  -b dc=example,dc=com contextCSN

dn: dc=example,dc=com
contextCSN: 20120201193408.178454Z#000000#000#000000

Provider と Consumer の出力
(上記の例では 20120201193408.178454Z#000000#000#000000)
が合致していれば正常に複製されている。
Provider で変更が行われる度にこの値は更新され、
Consumer でも同様の動作が実行される。

接続が遅い、または ldap データベースが巨大である場合、
consumer と provider の contextCSN の照合に
時間がかかることがある。
しかし consumer の contextCSN は着実に増えていく為
処理が進行中であることが判るだろう。

consumer の contextCSN が欠損している、
または provider に合致しない場合、
作業を中断してどこに問題が生じているのか確認する必要がある。
consumer の認証要求が成功している、
またはデータの取得要求(ldapsearch 文が大量に発行される)に
エラーが無い場合は provider の slapd (syslog) と
認証ログファイルを確認する。

単純なクエリが動作するかどうかを Consumer 上でテストする。
データベースの DN は:

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b dc=example,dc=com dn

以下の情報が出力される。

  • ‘john’ という名前のユーザ

  • ‘miners’ という名前のグループ

  • ‘People’ と ‘Groups’ という名前のノード

#アクセスコントロール
ユーザがリソースに対してどのような種類の操作
(読取り、書込み等)を行うことができるかを管理することを
アクセスコントロールと呼ぶ。
関連する設定ディレクティブは Access Control Lists
または ACL と呼称される。

slapd パッケージをインストールすると
さまざまな ACL が自動的に設定される。
既定で設定される ACL の中から 2 、 3 重要なものを見ていく。
ACL がどのように動作し、どのように設定されるのかを
理解する助けとなるだろう。

LDAP クエリの有効な ACL を取得する為には、
特別なフロントエンドデータベースのインスタンスと同様に、
問合せ先データベースの ACL 登録を参照する必要がある。
後述する ACL は、前者に合致しない場合は既定の ACL として動作する。
フロントエンドデータベースは二番目に参照され、
これら 2 つの ACL ソースの内最初に合致した ACL が適用される。
(“first match wins”)
下記のコマンドはそれぞれ hdb データベース(“dc=example,dc=com”)と
フロントエンドデータベースの ACL を取得する:

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config '(olcDatabase={1}hdb)' olcAccess

dn: olcDatabase={1}hdb,cn=config
olcAccess:
  {0}to attrs=userPassword,shadowLastChange by self
  write by anonymous
  auth by dn="cn=admin,dc=example,dc=com"
  write by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by self
  write by dn="cn=admin,dc=example,dc=com"
  write by * read

Note:
rootDN は常にデータベースの完全な権限を保有する。
ACL は明確な設定を提供するが、
slapd は動作制限を発生させる原因ともなる。

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config '(olcDatabase={-1}frontend)' olcAccess

dn: olcDatabase={-1}frontend,cn=config
olcAccess:
  {0}to * by dn.exact=gidNumber=0+uidNumber=0,
    cn=peercred,cn=external,cn=auth
  manage by * break
olcAccess: {1}to dn.exact="" by * read
olcAccess: {2}to dn.base="cn=Subschema" by * read

最初の ACL は重要である:

olcAccess:
  {0}to attrs=userPassword,shadowLastChange by self 
  write by anonymous
  auth by dn="cn=admin,dc=example,dc=com"
  write by * none

これはより簡便に表記することができる:

to attrs=userPassword
   by self write
   by anonymous auth
   by dn="cn=admin,dc=example,dc=com" write
   by * none

to attrs=shadowLastChange
   by self write
   by anonymous auth
   by dn="cn=admin,dc=example,dc=com" write
   by * none

これを組合せた ACL は下記を実行する:

  1. 匿名の ‘auth’ アクセスは最初の接続を行う為に
    userPassword 属性へ渡される。
    匿名で DIT へ接続する必要がない場合でも
    ‘by anonymous auth’ は必要となる。
    リモートエンドが一度接続されても認証が発生することがある。
    (次点を参照)

  2. 全ユーザが ‘read’ (‘by self write’ の為に) 権限を持ち、
    userPassword 属性へアクセスするが故に認証が発生することがある。

  3. 一方、 userPassword 属性は
    完全なアクセス権限を持つ rootDN を除いて
    他のユーザがアクセスすることはできない。

  4. ユーザが自身のパスワードを変更する為には
    passwd または他のユーティリティを使用する。
    shadowLastChange 属性は
    一度認証されたユーザの接続を許可する為に必要となる。

ACL が ‘by * read’ となっている為、
この DIT は匿名で検索することができる:

to *
   by self write
   by dn="cn=admin,dc=example,dc=com" write
   by * read

匿名で DIT を検索することを望まない場合は ACL を変更する必要がある。
バインド要求の際に認証を強制する場合は、
代わりに(または ACL の更新と組合せて)
‘olcRequire: authc’ ディレクティブを使用する。

前述のように slapd-config データベースの管理者アカウントは
自動作成されないが、 slapd-config データベースの
完全なアクセス権限を持つ SASL ID が存在する。
SASL ID はローカルホストのスーパーユーザ(root / sudo)を表す。
下記に例を示す:

dn.exact=gidNumber=0+uidNumber=0,
  cn=peercred,cn=external,cn=auth

下記のコマンドは slapd-config データベースの ACL を表示する。

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config '(olcDatabase={0}config)' olcAccess

dn: olcDatabase={0}config,cn=config
olcAccess:
  {0}to * by dn.exact=gidNumber=0+uidNumber=0,
    cn=peercred,cn=external,cn=auth manage by * break

これは SASL ID である為、
LDAP ユーティリティによる問合せを実行する際に
SASL 機構を使用する必要があり、本手引の中で頻出する。
SASL 機構は EXTERNAL 機構である。
例として先述のコマンドを見てみよう。下記に留意してもらいたい:

  1. ACL を照合する為に必ず sudo を使用して
    root ID へ切り替える必要がある。

  2. EXTERNAL 機構は IPC (UNIX domain sockets) を介して
    動作する為、必ず ldapi URI 形式を使用する必要がある。

全 ACL を簡単に取得するには下記のように操作する:

sudo ldapsearch
  -Q
  -LLL
  -Y EXTERNAL
  -H ldapi:///
  -b cn=config '(olcAccess=*)' olcAccess olcSuffix

アクセスコントロールについての詳細は
slapd.access のマニュアルページを参照。

#TLS
OpenLDAP サーバの認証を行う際は
暗号化セッションを使用することが望ましい。
暗号化セッションは Tranport Layer Security (TLS) を
使用することで実現することができる。

本章では自身の端末を認証局として使用し、
LDAP サーバ証明書(CA)を作成・署名する。
slapd は gnutils library を使用してコンパイルされている為、
certtool ユーティリティを使用してサーバ証明書を作成・署名する。

gnutls-bin と ssl-cert パッケージをインストールする:

sudo apt install gnutls-bin ssl-cert

認証局の秘密鍵を作成する:

sudo sh -c "certtool --generate-privkey > /etc/ssl/private/cakey.pem"

template/file /etc/ssl/ca.info を作成し、 CA を定義する:

/etc/ssl/ca.info
cn = Example Company
ca
cert_signing_key

自身で署名した CA 証明書を作成する:

sudo certtool
  --generate-self-signed 
  --load-privkey /etc/ssl/private/cakey.pem 
  --template /etc/ssl/ca.info 
  --outfile /etc/ssl/certs/cacert.pem

サーバの秘密鍵を作成する:

sudo certtool
  --generate-privkey
  --bits 1024
  --outfile /etc/ssl/private/ldap01_slapd_key.pem

Note:
ファイル名の ldap01 の部分は
サーバのホスト名に合わせて置換える。
ホストの証明書と鍵に名前を付け、
どのサービスに使用する証明書と鍵なのかを明確にする。

下記の情報を含む /etc/ssl/ldap01.info ファイルを作成する:

/etc/ssl/ldap01.info
organization = Example Company
cn = ldap01.example.com
tls_www_server
encryption_key
signing_key
expiration_days = 3650

上記の証明書は 10 年間有効となる。必要に応じて調整すること。

サーバの証明書を作成する:

sudo certtool
  --generate-certificate
  --load-privkey /etc/ssl/private/ldap01_slapd_key.pem
  --load-ca-certificate /etc/ssl/certs/cacert.pem
  --load-ca-privkey /etc/ssl/private/cakey.pem
  --template /etc/ssl/ldap01.info
  --outfile /etc/ssl/certs/ldap01_slapd_cert.pem

certinfo.ldif ファイルを下記の内容で作成する
( https://www.cacert.org を使用して作成した証明書を
例示する。必要に応じて調整すること):

certinfo.ldif
dn: cn=config
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/cacert.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/ldap01_slapd_cert.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/ldap01_slapd_key.pem

ldapmodify コマンドを使用して slapd へTLS が
slapd-config データベースを介して動作することを通知する:

sudo ldapmodify
  -Y EXTERNAL
  -H ldapi:///
  -f /etc/ssl/certinfo.ldif

暗号化を使用する為に /etc/default/slapd の中に
ldaps:// は必要ない:

/etc/default/slapd
SLAPD_SERVICES="ldap:/// ldapi:///"

Note:
LDAP の TLS/SSL (ldaps://) は
StartTLS の採用により廃止された。
既存の LDAP セッション(TCP ポート 389 番で待受ける)は
TLS/SSL によって保護されるようになった。
HTTPS と同様に LDAPS は TCP ポート 636 番で動作する
encrypted-from-the-start プロトコルとは異なる。

所有権と権限を制限する:

sudo adduser openldap ssl-cert
sudo chgrp ssl-cert /etc/ssl/private/ldap01_slapd_key.pem
sudo chmod g+r /etc/ssl/private/ldap01_slapd_key.pem
sudo chmod o-r /etc/ssl/private/ldap01_slapd_key.pem

OpenLDAP を再起動する:

sudo systemctl restart slapd.service

ホストのログ( /var/log/syslog )を確認し、
サーバが正しく起動しているか確認する。

#複製と TLS
サーバ間で複製を設定している場合、盗聴防止の為に
通信を暗号化(StartTLS)することが望ましい。
複製の通信の暗号化は、上記の認証における暗号化の使用とは異なる。
本章では TLS-aunthentication の動作環境を構築する。

複製の章に従って Provider と Consumer の間で複製を設定し、
TLS の章に従って Provider へ認証における
TLS を設定していることを前提とする。

上記では LDAP サービスにおいて
高い互換性を持つ複製の構築を目的としていた。
Provider 上で認証の TLS を設定している為、
Consumer 上でも同様に認証の TLS が必要とされる。
本章では複製の通信を暗号化する。
Provider 上で鍵 / 証明書を生成して他の CA 証明書を受付不可とし、
必要な材料を Consumer へ転送する。

Provider 上では格納用のディレクトリ(転送時に使用する)を作成し、
Consumer の秘密鍵を作成する:

mkdir ldap02-ssl
cd ldap02-ssl
sudo certtool
  --generate-privkey
  --bits 1024
  --outfile ldap02_slapd_key.pem

Consumer サーバ用に情報のファイルを作成し、
ldap02.info と名前を付ける。
値は必要に応じて調整すること:

ldap02.info
organization = Example Company
cn = ldap02.example.com
tls_www_server
encryption_key
signing_key
expiration_days = 3650

Consumer の証明書を作成する:

sudo certtool
  --generate-certificate
  --load-privkey ldap02_slapd_key.pem
  --load-ca-certificate /etc/ssl/certs/cacert.pem
  --load-ca-privkey /etc/ssl/private/cakey.pem
  --template ldap02.info
  --outfile ldap02_slapd_cert.pem

CA 証明書のコピーを取得する:

cp /etc/ssl/certs/cacert.pem .

以上で設定は完了し、 ldap02-ssl ディレクトリが
Consumer へ転送される。

scp を使用する(必要に応じて調整すること):

cd ..
scp -r ldap02-ssl user@consumer:

Consumer 上で TLS 認証を設定する:

sudo apt install ssl-cert
sudo adduser openldap ssl-cert
sudo cp ldap02_slapd_cert.pem cacert.pem /etc/ssl/certs
sudo cp ldap02_slapd_key.pem /etc/ssl/private
sudo chgrp ssl-cert /etc/ssl/private/ldap02_slapd_key.pem
sudo chmod g+r /etc/ssl/private/ldap02_slapd_key.pem
sudo chmod o-r /etc/ssl/private/ldap02_slapd_key.pem

/etc/ssl/certinfo.ldif ファイルを下記の内容で作成する
(必要に応じて調整すること):

/etc/ssl/certinfo.ldif
dn: cn=config
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/cacert.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/ldap02_slapd_cert.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/ldap02_slapd_key.pem

slapd-config データベースを設定する:

sudo ldapmodify
  -Y EXTERNAL
  -H ldapi:///
  -f certinfo.ldif

/etc/default/slapd
Provider (SLAPD_SERVICES) へ作成する。

Consumer 上で Consumer 側の複製用の TLS を設定する。
いくつか TLS オプションを設定し、
既存の olcSyncrepl 属性を更新する。
属性値の変更方法を下記に例示する。

consumer_sync_tls.ldif ファイルを下記の内容で作成する:

consumer_sync_tls.ldif
dn: olcDatabase={1}hdb,cn=config
replace: olcSyncRepl
olcSyncRepl: rid=0 provider=ldap://ldap01.example.com bindmethod=simple
binddn="cn=admin,dc=example,dc=com" credentials=secret
searchbase="dc=example,dc=com"
logbase="cn=accesslog"
logfilter="(&(objectClass=auditWriteObject)(reqResult=0))"
schemachecking=on type=refreshAndPersist retry="60 +" syncdata=accesslog
starttls=critical tls_reqcert=demand

追加のオプションを指定し、各 consumer が必ず StartTLS を使用し、
CA 証明書が Provider の ID 確認を要求するようにする。
IDIF シンタックスの属性(‘replace’)の値の変更にも留意すること。

ここまでの変更を反映する:

sudo ldapmodify
  -Y EXTERNAL
  -H ldapi:///
  -f consumer_sync_tls.ldif

slapd を再起動する:

sudo systemctl restart slapd.service

Provider 上で TLS セッションが確立されていることを確認する。
/var/log/syslog 内で ‘conns’-level logging の設定が
提供され、下記のようなメッセージが表示される:

/var/log/syslog
slapd[3620]: conn=1047 fd=20 ACCEPT
  from IP=10.153.107.229:57922 (IP=0.0.0.0:389)
slapd[3620]: conn=1047 op=0 EXT oid=1.3.6.1.4.1.1466.20037
slapd[3620]: conn=1047 op=0 STARTTLS
slapd[3620]: conn=1047 op=0 RESULT oid= err=0 text=
slapd[3620]: conn=1047 fd=20 TLS established tls_ssf=128 ssf=128
slapd[3620]: conn=1047 op=1 BIND
  dn="cn=admin,dc=example,dc=com" method=128
slapd[3620]: conn=1047 op=1 BIND
  dn="cn=admin,dc=example,dc=com"
  mech=SIMPLE ssf=0
slapd[3620]: conn=1047 op=1 RESULT tag=97 err=0 text

#LDAP 認証
LDAP サーバが稼働を始めたら、サーバへ通信を行う為のライブラリを
クライアントへインストールする必要がある。
Ubuntu では伝統的に libnss-ldap パッケージを
インストールすることでサーバへの通信を実現する。
libnss-ldap パッケージは、
設定の段階でユーザを補助するツール群を提供する。
下記を実行してパッケージをインストールする:

sudo apt install libnss-ldap

LDAP サーバの詳細を確認するプロンプトを表示する。
設定を間違えた場合は下記を実行することで修正することができる:

sudo dpkg-reconfigure ldap-auth-config

ダイアログの結果は /etc/ldap.conf に記録される。
サーバがメニューのオプションに対応していない場合は
/etc/ldap.conf を編集する。

NSS 用の LDAP プロファイルを設定する:

sudo auth-client-config -t nss -p lac_ldap

認証の為に LDAP が使用するシステムを設定する:

sudo pam-auth-update

メニューから LDAP と必要とする他の認証機構を選択する。
LDAP に基づいた資格を使用してログインが可能となる。
複製が使用されている場合、
LDAP クライアントは複数のサーバを参照する必要がある。
/etc/ldap.conf へ下記のように記述する:

/etc/ldap.conf
uri ldap://ldap01.example.com ldap://ldap02.example.com

Provider (ldap01) が応答しない場合、
要求はタイムアウトして Consumer (ldap02) への接続を試行する。

Samba ユーザを保存する為に LDAP を使用する場合、
Samba サーバを LDAP を使用して認証するように設定する必要がある。
詳細は Samba and LDAP を参照。

Note:
libnss-ldap パッケージの代替として
libnss-ldapd パッケージを使用することができるが、
不必要な nscd パッケージがインストールされる。
不要なパッケージはインストール後に削除する。

#ユーザとグループの管理
ldap-utils パッケージはディレクトリの管理に必要な
ユーティリティを提供するが、長いオプションを必要とする為
ユーザにとって負担となることがある。
ldapscripts パッケージは
ldap-utils のラッパースクリプトを有しており、
より簡単に使用することができる。

パッケージをインストールする:

sudo apt install ldapscripts

/etc/ldapscripts/ldapscripts.conf を編集し、
下記の記述を検索する:

/etc/ldapscripts/ldapscripts.conf
SERVER=localhost
BINDDN='cn=admin,dc=example,dc=com'
BINDPWDFILE="/etc/ldapscripts/ldapscripts.passwd"
SUFFIX='dc=example,dc=com'
GSUFFIX='ou=Groups'
USUFFIX='ou=People'
MSUFFIX='ou=Computers'
GIDSTART=10000
UIDSTART=10000
MIDSTART=10000

ldapscripts.passwd ファイルを作成し、
rootDN へディレクトリへのアクセスを許可する。

sudo sh -c "echo -n 'secret' > /etc/ldapscripts/ldapscripts.passwd"
sudo chmod 400 /etc/ldapscripts/ldapscripts.passwd

Note:
“secret” の部分をデータベースの
rootDN ユーザの実際のパスワードへ置換する。

スクリプトからディレクトリの管理を行う準備が整った。
使い方の例を下記に示す:

新しいユーザを作成する:

sudo ldapadduser george example

この例では george という uid のユーザを作成し、
ユーザのプライマリグループ(gid)を example に設定する。

ユーザのパスワードを変更する:

sudo ldapsetpasswd george
Changing password for user uid=george,ou=People,dc=example,dc=com
New Password:
New Password (verify):

ユーザを削除する:

sudo ldapdeleteuser george

グループを登録する:

sudo ldapaddgroup qa

グループを削除する:

sudo ldapdeletegroup qa

グループへユーザを登録する:

sudo ldapaddusertogroup george qa

memberUid 属性を参照すると qa グループの値として
george が記録されている。

グループからユーザを削除する:

sudo ldapdeleteuserfromgroup george qa

memberUid 属性が qa グループから削除される。

ldapmodifyuser スクリプトはユーザの属性を
登録、削除、または更新する。
ldapmodifyuser スクリプトは
ldapmodify ユーティリティと同じシンタックスを使用する。
例えば:

sudo ldapmodifyuser george
# 下記の登録を更新する:
dn: uid=george,ou=People,dc=example,dc=com
objectClass: account
objectClass: posixAccount
cn: george
uid: george
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/george
loginShell: /bin/bash
gecos: george
description: User account
userPassword:: e1NTSEF9eXFsTFcyWlhwWkF1eGUybVdFWHZKRzJVMjFTSG9vcHk=

# 更新内容を記述し、 CTRL-D で終了する。
dn: uid=george,ou=People,dc=example,dc=com
replace: gecos
gecos: George Carlin

ユーザの gecos は “George Carlin” となっている。

ldapscripts はテンプレートシステムを備えている。
テンプレートは下記の属性のカスタマイズを可能にする。

  • user
  • group
  • machine objects

例えば user テンプレートを有効にするには
/etc/ldapscripts/ldapscripts.conf を編集する:

/etc/ldapscripts/ldapscripts.conf
UTEMPLATE="/etc/ldapscripts/ldapadduser.template"

/usr/share/doc/ldapscripts/examples ディレクトリに
サンプルテンプレートが格納されている。 ldapadduser.template.sample ファイルを
コピーまたは名前を変更して
/etc/ldapscripts/ldapadduser.template を作成する:

sudo cp
  /usr/share/doc/ldapscripts/examples/ldapadduser.template.sample
  /etc/ldapscripts/ldapadduser.template

新しいテンプレートを編集して独自の属性を登録する。
下記の例では inetOrgPerson の objectClass を使用して
新規ユーザを作成する:

/etc/ldapscripts/ldapadduser.template
dn: uid=<user>,<usuffix>,<suffix>
objectClass: inetOrgPerson
objectClass: posixAccount
cn: <user>
sn: <ask>
uid: <user>
uidNumber: <uid>
gidNumber: <gid>
homeDirectory: <home>
loginShell: <shell>
gecos: <user>
description: User account
title: Employee

<ask> オプションは sn 属性用に使用されている。
これは ldapadduser へ値を入力するプロンプトを追加する。

パッケージが提供するユーティリティの一覧を掲載する:

  • ldaprenamemachine
  • ldapadduser
  • ldapdeleteuserfromgroup
  • ldapfinger
  • ldapid
  • ldapgid
  • ldapmodifyuser
  • ldaprenameuser
  • lsldap
  • ldapaddusertogroup
  • ldapsetpasswd
  • ldapinit
  • ldapaddgroup
  • ldapdeletegroup
  • ldapmodifygroup
  • ldapdeletemachine
  • ldaprenamegroup
  • ldapaddmachine
  • ldapmodifymachine
  • ldapsetprimarygroup
  • ldapdeleteuser

#バックアップと復元
ldap を思いのままに操作できるようになったところで、
これまでに行った全操作を保存し、必要な時に復元ができるようにする。

ldap データベース、具体的にはバックエンド(cn=config)と
フロントエンド(dc=example,dc=com)の
バックアップを取得する方法が必要となる。
上記のデータベースのバックアップを
/export/backup へ出力する場合、
下記のスクリプトに例示するように slapcat を使用することができる。
このスクリプトに /usr/local/bin/ldapbackup と名前を付ける:

/usr/local/bin/ldapbackup
#!/bin/bash

BACKUP_PATH=/export/backup
SLAPCAT=/usr/sbin/slapcat

nice ${SLAPCAT} -n 0 > ${BACKUP_PATH}/config.ldif
nice ${SLAPCAT} -n 1 > ${BACKUP_PATH}/example.com.ldif
nice ${SLAPCAT} -n 2 > ${BACKUP_PATH}/access.ldif
chmod 640 ${BACKUP_PATH}/*.ldif

Note:
これらのファイルは非圧縮テキストファイルであり、
ldap データベースに含まれる
ツリーレイアウト、ユーザ名、全パスワードを含む。
従って /export/backup は暗号化されたパーティションへ作成し、
スクリプトは作成したファイルを暗号化することが望ましい。
理想としては両者を行うべきだが、セキュリティ要件に応じて決定する。

cron スクリプトへこのプログラムを自動実行するように登録する。
頻度としては日に一回で事足りるが、
場合によってはもっと高い頻度が要求されることもある。
cron スクリプトの例を下記に示す。
/etc/cron.d/ldapbackup と名前を付け、
毎夜 22:45h: に実行する:

/etc/cron.d/ldapbackup
MAILTO=backup-emails@domain.com
45 22 * * *  root    /usr/local/bin/ldapbackup

作成されたファイル群はバックアップサーバへコピーされる。

ldap の再インストールを行う場合は
下記のように復元処理を行うことができる:

sudo systemctl stop slapd.service
sudo mkdir /var/lib/ldap/accesslog
sudo slapadd -F /etc/ldap/slapd.d -n 0 -l /export/backup/config.ldif
sudo slapadd -F /etc/ldap/slapd.d -n 1 -l /export/backup/domain.com.ldif
sudo slapadd -F /etc/ldap/slapd.d -n 2 -l /export/backup/access.ldif
sudo chown -R openldap:openldap /etc/ldap/slapd.d/
sudo chown -R openldap:openldap /var/lib/ldap/
sudo systemctl start slapd.service

#参考資料

  1. オンラインドキュメント

  2. slapd パッケージには数多くのマニュアルページが存在する。
    下記に重要なマニュアルページを列記する:

    • slapd
    • slapd-config
    • slapd.access
    • slapo-syncprov
  3. 他のマニュアルページ:

    • auth-client-config
    • pam-auth-update
  4. Zytrax’s LDAP for Rocket Scientists;
    簡潔かつ包括的な LDAP の扱い方

  5. Ubuntu community OpenLDAP wiki page は
    資料のコレクションを有している。

  6. O’Reilly’s LDAP System Administration
    (textbook; 2003)

  7. Packt’s Mastering OpenLDAP (textbook; 2007)

34
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?