Yet another OpenLDAP チュートリアルです。
以下のバージョンを対象に設定例を書きます。
ソフトウェア | バージョン | この記事の設定例で使用するポート | 備考 |
---|---|---|---|
OpenLDAP | 2.4.44 | 389 | |
GitBucket | 4.20.0 | 8081 | |
Apache | 2.4.29 | 8083 | |
Subversion | 1.9.7 (r1800392) | 8084 | |
Redmine | 3.4.4.stable | 8085 | |
Rocket.Chat | 0.62.0 | 8086 | |
Knowledge | 1.12.0 | 8082 | |
Wekan | 2.13 (commit-id: 6f0e074) | 8087 | ※ 2019/2/2 追記 |
目的
- グループ内で利用しているWebサービスが増えてきたので、ユーザ管理が面倒臭くなって来た
- LDAPでユーザを一元管理しよう
- 既存のActiveDirectory (Windows)を弄るのは全社的に影響があるので、グループ内にOpenLDAPを立てて、グループ内サービスの認証はこれを使おう
- 知見が溜まったら、ActiveDirectoryも試して完全な一元管理にしたいね
私はLDAPに関してはほとんど何もわからないが、なんとかOpenLDAPでの一元管理が出来そうな見込みが立ったのでここにその記録を残すことにした。LDAPに関する詳しい説明をするわけではない。とにかく構築して利用したいのである。
バックアップとか冗長化とかセキュア通信とかは他の記事に譲る。ここでは触れない。
LDAPの用語
属性 | 説明 |
---|---|
dc | ドメイン構成要素(Domain Component) |
ou | 組織単位(Organization Unit) |
cn | 一般名(Common Name) |
まず、このあたりの用語が非常に取っつきにくい。略語すぎて区別がつきにくいと言うべきか。だから、混乱を避けるためこれを必要最低限とし、DNとかDITとか上記以外の用語は説明文中では使わないことにした。
LDAPの構成
dcとかouがなんなのか、既存の自組織の要素と照らし合わせて整理する。
既存要素 | 通常の表記 | LDAPでの表記 |
---|---|---|
社内ドメイン | hoge.com | dc=hoge,dc=com |
部 | XXX部 | ou=XXX,dc=hoge,dc=com |
課 | YYY課 | ou=YYY,ou=XXX,dc=hoge,dc=com |
社員A | Aさん | cn=userA,ou=YYY,ou=XXX,dc=hoge,dc=com |
例えば、Aさんのメールアドレスはこの例だと userA@hoge.com
となるだろう。
それとは別に組織的な階層をouであらわしている。dcとouが別々の概念で現れる理由はよくはわからないが、(別にYYY.XXX.hoge.com というドメインがあっても良いわけだが)ドメインはコンピュータ上の構成の都合(メールアドレスとかコンピュータ名とか)で、それとは独立して実組織の都合を反映したいためにouがあるのではないかと思う。(本当のところは知らない)
hoge.comが既存のドメインだとして、今回の目的はそれと重複しないようにOpenLDAPを構築したい。通常なら、既存のドメインに従属するようにグループ内のドメインを fuga.hoge.com (dc=fuga,dc=hoge,dc=com) とかにするのが良いのかもしれない。
ただ、今回は閉じたネットワーク内でユーザ認証を一元管理したいだけなので、以下のように完全に独立した単純な構成とする。
| 識別名
-----+------
ドメイン | dc=hoge,dc=local
所属 | ou=Users,dc=hoge,dc=local
人 | cn=userA,ou=Users,dc=hoge,dc=local
ドメインは、もっと単純にdc=localだけでもいいし、実際の運用はそうしている。ただ、階層のある組織の例を示した方が設定の記述の例として有効だと思うのでこのようにした。
ouとcnの違いもわかりにくいが、ファイル構造で考えれば、ouはディレクトリ、cnはファイルと考えればわかりやすいのではないかと思う。ouは階層構造とcnの集合を表し、cnはデータを保持する。
また、cnはクラスを持っていてクラスによってどのようなデータ(メールアドレスとか姓名とか)を持つかがあらかじめ決まっていたりする。この辺りは実例のなかで扱う。
OpenLDAP を立ち上げる
この記事では OpenLDAP(以下、openldapと書く)やそれを利用するサービスをDockerを使って構築する。
openldap は、osixia/openldapのイメージを使うこととした。
まず、私のホームディレクトリにdocker/openldapディレクトリを作成し、その配下に以下のファイルを作る
$HOME
`-- docker
`-- openldap
`-- docker-compose.yml
version: '2'
services:
app:
image: osixia/openldap
container_name: openldap
environment:
LDAP_ORGANISATION: "HogeHoge Group"
LDAP_DOMAIN: "hoge.local"
LDAP_ADMIN_PASSWORD: "changeme"
LDAP_CONFIG_PASSWORD: "changeme2"
ports:
- "389:389"
volumes:
- ./db:/var/lib/ldap
- ./slapd.d:/etc/ldap/slapd.d
- ./ldif:/ldif
app2:
image: osixia/phpldapadmin
container_name: phpldapadmin
environment:
PHPLDAPADMIN_LDAP_HOSTS: openldap
PHPLDAPADMIN_HTTPS: "false"
ports:
- "8080:80"
depends_on:
- app
dockerでLDAP Serverを起動。ついでにphpLDAPadminもを参考に、phpLDAPadminも立ち上げるようにした。しかし、phpLDAPadminは決して使いやすいものではないので、手動でエントリを編集したい時や削除したいときとかにしか使ってない。
構築及び起動の手順は以下の通り。
$ cd ~/docker/openldap
$ docker-compose up
これで、起動に成功すると db, slapd.d ディレクトリが作成され、そこにファイルができる。別のシェルを立ち上げて以下のように各ディレクトリの中身を見るとファイルが出来ていることが確認できる。
$HOME
`-- docker
`--openldap
|-- db/
|-- docker-compose.yml
|-- ldif/
`-- slapd.d/
$ ls db
DB_CONFIG __db.002 alock dn2id.bdb entryUUID.bdb log.0000000001
__db.001 __db.003 cn.bdb entryCSN.bdb id2entry.bdb objectClass.bdb
$ ls slapd.d/
cn=config/ cn=config.ldif docker-openldap-was-started-with-tls
db, slapd.dはそれぞれ、DBの構成ファイル(デフォルトではBerkeleyDB)と設定ファイルの内容になっており、コンテナを作り直してもデータの永続化がこのディレクトリで行えている。ldif というディレクトリもできているが、これについては後述する。
もし、いろいろと設定を行ったあとに最初からやり直したい場合は
$ docker-compose stop
$ docker-compose rm
$ rm -rf db slapd.d # 場合によっては、sudo rm …としないとダメかも
$ docker-compose up
とすればよい。
ou(所属) と cn(人)の追加
最初に示したように、ou=Users グループと、cn=userA ユーザのデータを追加してみる。
| 識別名
----|-----
所属 | ou=Users,dc=hoge,dc=local
人 | cn=userA,ou=Users,dc=hoge,dc=local
ou=Users を作成するには、まず先ほど作成された ldif ディレクトリに以下のファイルを作成する。
dn: ou=Users,dc=hoge,dc=local
objectClass: organizationalUnit
そして、以下のコマンドを実行する。オプションの細かな説明はこの記事の最後にまとめてあるので適宜参照して欲しい。
$ ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/ou-users.ldif
adding new entry "ou=Users,dc=hoge,dc=local"
Mac は標準で ldapadd コマンドを持っているので、このようにホスト側でそのまま実行できる。しかし、そのような環境でない場合はコンテナで実行する必要がある。コンテナで実行するには以下のようにすればよい。
$ docker exec -it openldap bash # ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/ou-users.ldif
または、
$ echo "ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/ou-users.ldif" | docker exec -i openldap bash
以降、コマンド実行はホスト側で実行するかのように記述するが環境に合わせて読み替えて欲しい。
ldif ディレクトリは、ホストとコンテナ両方でコマンドが実行できるようファイル置き場をここに決めているというだけである。本当はなんでも良い。(ldif ディレクトリは、docker-compose.ymlファイルにて、コンテナの/ldif にマップするよう定義してあるので双方で参照できる)
検索してみる。正しく追加されている。
$ ldapsearch -LLL -D cn=admin,dc=hoge,dc=local -w changeme -b dc=hoge,dc=local
dn: ou=Users,dc=hoge,dc=local
objectClass: organizationalUnit
ou: Users
ou=Users 配下に userA を追加する
まずは、パスワードをhogehoge
だとして、そのハッシュ値を得る。
$ slappasswd -h '{MD5}' -s hogehoge
{MD5}MpQ15eZr6AmmVq8QX0JAHg==
ここで、表示されたハッシュ値を以下のファイルの userPassword
に貼り付ける。面倒なので以降この記事で設定するパスワードはこれを利用する。
dn: cn=userA,ou=Users,dc=hoge,dc=local
objectClass: inetOrgPerson
displayName: User A
sn: User
mail: usera@hoge.local
userPassword: {MD5}MpQ15eZr6AmmVq8QX0JAHg==
$ ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/cn-userA.ldif
sn は SirName で名前の姓を設定する属性である。この属性はinetOrgPersonクラスで必須として定義されてあるので何かしら書かなければならない。
mail は任意属性だが色々なWebサービスで利用されるので追加した。グループ内でメールを利用していない場合は必要ない。
追加したユーザを削除するには、追加した時に使ったLDIFファイルのうち、dn: の行の中身を指定する。(dn:
を書かないことに注意)
$ ldapdelete -D cn=admin,dc=hoge,dc=local -w changeme
〜以下、キーボードからの入力(^D は、Ctrl+Dを押したことを示す)〜
cn=userA,ou=Users,dc=hoge,dc=local
^D
修正(エントリの部分的な変更)を行う方法は面倒くさいので、削除と追加で対応するか、phpldapadminを使うと良い。(phpldapadminについてはこの記事では説明しない)
ユーザの追加方法を示したが、利用するサービスによって必要な項目は変わってくる。以下、例としていろいろなユーザを作ってみる。
ユーザIDのみを持ったユーザの例
dn: uid=userB,ou=Users,dc=hoge,dc=local
objectClass: account
$ ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/cn-userB.ldif
dn の行が cn で始まらないことに注意。結局cnとはなんなのか・・・と思ってしまうが、accountクラスは低レベルな定義情報だからcnを持たないのだろうと思う。また、uidとuseridは同じ意味(useridはuidのエイリアス)として扱われる。
そのため(userid=userB)のようにフィルタを指定して検索しても、uid=userBのエントリを見つけることができる。
$ ldapsearch -LLL -D cn=admin,dc=hoge,dc=local -w changeme -b "ou=Users,dc=hoge,dc=local" "(userid=userB)"
dn: uid=userB,ou=Users,dc=hoge,dc=local
objectClass: account
uid: userB
ユーザID、パスワードを持ったユーザの例
objectClass を複数書いた例、accountクラスはuidを持ち、simpleSecurityObjectクラスはuserPasswordを持っている。
dn: uid=userC,ou=Users,dc=hoge,dc=local
objectClass: account
objectClass: simpleSecurityObject
userPassword: {MD5}MpQ15eZr6AmmVq8QX0JAHg==
ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/cn-userC.ldif
ユーザID、パスワードを持ったユーザの例2
dn: cn=userD,ou=Users,dc=hoge,dc=local
objectClass: person
objectClass: simpleSecurityObject
sn: user
userPassword: {MD5}MpQ15eZr6AmmVq8QX0JAHg==
なんとなく、cnを使いたいと思ったので、personクラスを使ってみた例。これもsn(姓)が必須になってしまう。あと、さらに電話番号などもう少し任意の属性がほしいと思ったらorganizationalPersonクラスというのもある。これは、personを継承している。
ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/cn-userD.ldif
ユーザID、パスワード、メールアドレスを持ったユーザの例
inetOrgPersonクラスを使うのが良いだろう。これは、organizationalPersonクラスを継承している。なお、この例となるユーザは定義済みのuserAであるが、以下の例は下の名前(givenName)も追加してさらに漢字の名前にしてみた。漢字はUTF-8で書くと良い。
dn: cn=userA,ou=Users,dc=hoge,dc=local
objectClass: inetOrgPerson
displayName: 愛 上雄
sn: 愛
givenName: 上雄
mail: usera@hoge.local
userPassword: {MD5}MpQ15eZr6AmmVq8QX0JAHg==
UNIXアカウントの例
dn: cn=userE,ou=Users,dc=hoge,dc=local
objectClass: account
objectClass: posixAccount
uid: userE
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/userE
userPassword: {MD5}MpQ15eZr6AmmVq8QX0JAHg==
loginShell: /bin/bash
gecos: user E
description: 説明
posixAccountクラスは単体ではcnを作れない補助用のクラスとして定義されているので、accountと組み合わせて使っている。
gecos は、歴史的にコメントやフルネームを書くフィールドとして使われてきた。descriptionはUNIXアカウントが持つ情報とは別にLDAPとして説明欄を持つためにあると思われる。
UNIXアカウントは作るがログインさせたくない場合は loginShell を/bin/falseにすれば良い。
Sambaユーザ
(調べてから追記する予定)
各サービスのLDAP設定例
以降の設定例では、cnをユーザIDとするので、userA,userD,userEだけが使える。userBのような例も設定を変えれば使えるかもしれないが試してはいない。
各サービスのコンテナからopenldapコンテナに直接接続するような設定をしても良いが、設定が冗長になるのでこの記事では行わない。このため設定にホストのIPアドレスを直接書かなくてはならない。
ホストのIPアドレスを埋め込みたくない場合は、openldapと同一のdocker-compose.ymlにサービスの定義を記述するか、docker-compose で別の docker-compose.yml で作ったコンテナとリンクする (ネットワークを繋げる)を参考にすると良さそう。
GitBucket
version: '2'
services:
app:
container_name: gitbucket
image: takezoe/gitbucket
ports:
- "8081:8080"
- "29418:29418"
volumes:
- ./gitbucket:/gitbucket
db:
container_name: postgres
image: postgres:9.5-alpine
environment:
POSTGRES_DB: gitbucket
POSTGRES_USER: gitbucket
POSTGRES_PASSWORD: gitbucket
volumes:
- ./db:/var/lib/postgresql/data
$ docker-compose up
項目 | 値 |
---|---|
URL | http://localhost:8081/ |
デフォルトの管理者ユーザ | root |
デフォルトのパスワード | root |
管理者でログインし、右上のアカウントアイコンからSystem Administration -> System settings -> Authentication LDAP にチェックをつけ、以下の設定を入れる。
項目 | 設定値 |
---|---|
LDAP host | openLDAPを起動したホスト側のIPアドレス(gitbucketコンテナ内からの接続のため、localhost は使えない) |
LDAP port | 389 |
Bind DN | cn=admin,dc=hoge,dc=local |
Bind password | changeme |
Base DN | ou=Users,dc=hoge,dc=local |
User name attribute | cn |
Additional filter condition | 空欄 |
Full name attribute | displayName |
Mail address attribute | mail または cn |
Enable TLS | チェックしない |
Enable SSL | チェックしない |
Gitbucket はメールアドレスが必須となる。userDのようなmail属性を持たないユーザだとログイン後メールアドレスの登録を促される。ただし、実際にメールアドレスの形式 (xxx@xxxx.xxx
)である必要はない。そこで、グループ内でメールを使っていない場合は、Mail address attribute に、cn など適当な属性をあてると良い。
Apache
以降のSubversionの設定のために先にApacheの設定について書く
Apacheの場合、複数モジュールを活用しなければならないので順を追って確認していく。
まず、以下のファイルを作成し、認証のない単純な構成を作る。
version: '2'
services:
app:
image: httpd:alpine
volumes:
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf
- ./htdocs:/usr/local/apache2/htdocs
ports:
- 8083:80
httpd.conf は、apache:alpine のhttpd.confからコメントを削り、必要最低限に絞り込んだ以下を例に使用する。
ServerRoot "/usr/local/apache2"
Listen 80
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
<IfModule unixd_module>
User daemon
Group daemon
</IfModule>
ServerAdmin you@example.com
<Directory />
AllowOverride none
Require all denied
</Directory>
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
<Files ".ht*">
Require all denied
</Files>
ErrorLog /proc/self/fd/2
LogLevel warn
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog /proc/self/fd/1 common
この状態で、htdocsディレクトリを作成し、dockerでサービスを起動する。
$ mkdir htdocs
$ docker-compose up
そして、http://localhost:8083/ を開いて空のディレクトリが参照できることを確認する。
次に、普通のファイル認証を試す。htpasswd コマンドで .htpasswd ファイルを作成し、それを認証に使用する。ユーザはuserF、パスワードはhogehogeとする。
$ htpasswd -nb userF hogehoge > htdocs/.htpasswd
$ chmod 600 htdocs/.htpasswd
そして、httpd.confに以下の修正を加える
--- httpd.conf.orig 2018-03-02 07:46:08.000000000 +0900
+++ httpd-file.conf 2018-03-02 08:08:00.000000000 +0900
@@ -7,6 +7,10 @@
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule authz_core_module modules/mod_authz_core.so
+LoadModule authn_core_module modules/mod_authn_core.so
+LoadModule authz_user_module modules/mod_authz_user.so
+LoadModule auth_basic_module modules/mod_auth_basic.so
+LoadModule authn_file_module modules/mod_authn_file.so
<IfModule unixd_module>
User daemon
@@ -23,7 +27,12 @@
<Directory "/usr/local/apache2/htdocs">
Options Indexes FollowSymLinks
AllowOverride None
- Require all granted
+
+ AuthType Basic
+ AuthBasicProvider file
+ AuthUserFile /usr/local/apache2/htdocs/.htpasswd
+ AuthName "password file authentication"
+ Require valid-user
</Directory>
<IfModule dir_module>
認証に関連するモジュールは以下の通り
モジュール名 | 説明 |
---|---|
authz_core_module | Requireの利用に必要 |
authn_core_module | AuthTypeの利用に必要 |
authz_user_module | Require valid-userの利用に必要 |
auth_basic_module | AuthBasicProviderの利用に必要 |
authn_file_module | AuthBasicProvider fileの利用に必要 |
再度、http://localhost:8083/ を開いてuserFでBasic認証されることを確認する。
基本的な設定が確認できたところで、LDAP認証を試す。
ApacheでLDAP認証をするには以下のモジュールが必要になる。
モジュール名 | 説明 |
---|---|
mod_authnz_ldap | LDAP認証のためのモジュール |
mod_ldap | LDAP操作用のモジュール |
これらを有効にするため httpd.conf に以下の修正を加える。ところで、差分で例を示すのは、既存の環境にどのような差分を適用すれば良いかわかるようにすることを狙っている。
--- httpd-file.conf 2018-03-02 08:08:00.000000000 +0900
+++ httpd.conf 2018-03-02 08:08:12.000000000 +0900
@@ -10,7 +10,8 @@
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule auth_basic_module modules/mod_auth_basic.so
-LoadModule authn_file_module modules/mod_authn_file.so
+LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
+LoadModule ldap_module modules/mod_ldap.so
<IfModule unixd_module>
User daemon
@@ -29,9 +30,12 @@
AllowOverride None
AuthType Basic
- AuthBasicProvider file
- AuthUserFile /usr/local/apache2/htdocs/.htpasswd
- AuthName "password file authentication"
+ AuthBasicProvider ldap
+ AuthName "LDAP authentication"
+ AuthLDAPURL ldap://ホストのIPアドレス/ou=Users,dc=hoge,dc=local?cn
+
+ AuthLDAPBindDN cn=admin,dc=hoge,dc=local
+ AuthLDAPBindPassword changeme
Require valid-user
</Directory>
項目 | 設定値 | 説明 |
---|---|---|
AuthBasicProvider | ldap | LDAP認証を利用する指定 |
AuthName | "LDAP authentication" | 任意の文字列 |
AuthLDAPURL | ldap://ホストのIPアドレス/ou=Users,dc=hoge,dc=local?cn | LDAPサーバのURL。URLでBaseDNや認証に利用するIDも指定している |
AuthLDAPBindDN | cn=admin,dc=hoge,dc=local | LDAPサーバ管理者ユーザ(BindDN) |
AuthLDAPBindPassword | changeme | LDAPサーバ管理者パスワード |
Require | valid-user | 認証されたユーザのみ許可する指定、ここで細かくユーザの条件を指定することもできる |
再度、http://localhost:8083/ を開いて今度はuserAでBasic認証されることを確認する。
最後に、ldapとfile認証の組み合わせを示す。
--- httpd-ldap.conf 2018-03-02 08:08:12.000000000 +0900
+++ httpd.conf 2018-03-02 08:17:54.000000000 +0900
@@ -12,6 +12,7 @@
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
LoadModule ldap_module modules/mod_ldap.so
+LoadModule authn_file_module modules/mod_authn_file.so
<IfModule unixd_module>
User daemon
@@ -30,12 +31,14 @@
AllowOverride None
AuthType Basic
- AuthBasicProvider ldap
- AuthName "LDAP authentication"
+ AuthBasicProvider ldap file
+ AuthName "LDAP or password file authentication"
AuthLDAPURL ldap://ホストのIPアドレス/ou=Users,dc=hoge,dc=local?cn
+ AuthUserFile /usr/local/apache2/htdocs/.htpasswd
AuthLDAPBindDN cn=admin,dc=hoge,dc=local
AuthLDAPBindPassword changeme
+ #AuthLDAPBindAuthoritative off
Require valid-user
</Directory>
これで、LDAPに存在しないユーザは.htpasswdファイルで認証される。
これを試したのは以下のディレクティブを検証したかったからである。
項目 | 設定値 | 説明 |
---|---|---|
AuthLDAPBindAuthoritative | on(デフォルト) | 今回の例で説明すると、onの場合、LDAPに存在しないユーザのみパスワードファイルの認証が試される。(だからuserFでログインできる)offの場合、例えばuserAでの認証でパスワードが違った場合でも、パスワードファイルでの認証が試される。userFの認証についてはどちらでも変わらない。 |
よく、このディレクティブをoffにする例を見るが、複数の認証機構で重複してユーザを管理したい場合に必要な設定であり、基本はデフォルト(on)のままでもいいのではないかと思う。また、このディレクティブは、AuthzLDAPAuthoritative, AuthLDAPAuthoritative など名前の変遷があったようで結構混乱する。
Subversion
まずはパスワードファイルでの認証を行うSubversionリポジトリを構築する。
以下は、カレンティディレクトリに検証用のsvn/testというリポジトリを作るスクリプト。
カレントディレクトリのsvn, testというディレクトリを最初に消しに行くので注意。
set -ex
rm -rf svn test
mkdir svn
echo websvn.txt short message > svn/websvn.txt
touch svn/svn.htpasswd
htpasswd -b svn/svn.htpasswd userF hogehoge
chmod 600 svn/svn.htpasswd
#chown 1000:1000 svn/svn.htpasswd
svnadmin create svn/test
svn co file://$PWD/svn/test test
( cd test
echo hello svn > README.md
svn add README.md
svn commit -m 'test'
)
rm -rf test
$ sh ./setup-svnrepo.sh
上記で、パスワード認証用のファイル svn/svn.htpasswd ファイルも作成している。
次にdocker環境を構築する。前のhttpd:alpineを元に、subversionを追加したイメージを以下で作成する。これで、インストールするsubversionは執筆時点で1.9.7になるようだ。
FROM httpd:alpine
RUN apk --no-cache add mod_dav_svn subversion
version: '2'
services:
app:
build: .
volumes:
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf
- ./htdocs:/usr/local/apache2/htdocs
- ./svn:/usr/local/apache2/svn
ports:
- 8084:80
そして、 httpd.conf は以下の通りとなる。httpd:alpine のイメージはapacheをビルドしているため、/usr/local/apache2にインストールされているが、apkでインストールしたmod_dav_svnは/usr/lib/apache2にインストールされるらしい。LoadModuleのところでdav_svn_module,authz_svn_moduleモジュールのパスが違うのはそのためである。
ServerRoot "/usr/local/apache2"
Listen 80
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule dav_module modules/mod_dav.so
LoadModule dav_svn_module /usr/lib/apache2/mod_dav_svn.so
#LoadModule authz_svn_module /usr/lib/apache2/mod_authz_svn.so
<IfModule unixd_module>
User daemon
Group daemon
</IfModule>
ServerAdmin you@example.com
<Directory />
AllowOverride none
Require all denied
</Directory>
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
<Location /svn>
DAV svn
SVNParentPath /usr/local/apache2/svn
SVNListParentPath on
AuthType Basic
AuthBasicProvider file
AuthUserFile /usr/local/apache2/svn/svn.htpasswd
AuthName "Subversion Repository"
#AuthzSVNAccessFile /usr/local/apache2/svn/svn.authz
Require valid-user
</Location>
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
<Files ".ht*">
Require all denied
</Files>
ErrorLog /proc/self/fd/2
LogLevel warn
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog /proc/self/fd/1 common
AuthzSVNAccessFile (authz_svn_moduleモジュール) はsvnのフォルダ毎に細かくアクセス権を設定するためのものだが、今回の例では使わないことにする。
この状態で、以下でイメージのビルドと起動を行う。
$ docker-compose up
そして、 http://localhost:8084/svn/test がuserFで認証できることを確認する。
次に、LDAP認証を試す。 やることはApacheの場合と同じである。httpd.conf は以下のように修正する。
--- httpd.conf.orig 2018-03-02 11:25:04.000000000 +0900
+++ httpd.conf 2018-03-02 11:26:56.000000000 +0900
@@ -14,6 +14,8 @@
LoadModule dav_module modules/mod_dav.so
LoadModule dav_svn_module /usr/lib/apache2/mod_dav_svn.so
#LoadModule authz_svn_module /usr/lib/apache2/mod_authz_svn.so
+LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
+LoadModule ldap_module modules/mod_ldap.so
<IfModule unixd_module>
User daemon
@@ -39,10 +41,14 @@
SVNListParentPath on
AuthType Basic
- AuthBasicProvider file
- AuthUserFile /usr/local/apache2/svn/svn.htpasswd
+ AuthBasicProvider ldap
AuthName "Subversion Repository"
#AuthzSVNAccessFile /usr/local/apache2/svn/svn.authz
+
+ AuthLDAPURL ldap://ホストのIPアドレス/ou=Users,dc=hoge,dc=local?cn
+ AuthLDAPBindDN cn=admin,dc=hoge,dc=local
+ AuthLDAPBindPassword changeme
+
Require valid-user
</Location>
$ docker-compose up
で、起動して、 http://localhost:8084/svn/test をuserAで認証できることを確認する。
また、userAでチェックアウトできることも確認する。
$ svn co --username=userA http://localhost:8084/svn/test/
Redmine
version: '2'
services:
app:
image: redmine
ports:
- 8085:3000
environment:
REDMINE_DB_MYSQL: db
REDMINE_DB_PASSWORD: example
depends_on:
- db
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: redmine
volumes:
- ./db:/var/lib/mysql
以下で起動する。最初に db ディレクトリを作っておく必要があることに注意。
$ mkdir db
$ docker-compose up
項目 | 値 |
---|---|
URL | http://localhost:8085/ |
デフォルトの管理者ユーザ | admin |
デフォルトのパスワード | admin |
管理者でログインし、左上のメニュー「管理」->「LDAP認証」を選ぶ。そして、「新しい認証方式」をクリックし、以下の設定を入れる。
項目 | 設定値 |
---|---|
名称 | openldap (なんでもいい) |
ホスト | openLDAPを起動したホスト側のIPアドレス |
ポート | 389 (LDAPSのチェックはつけない) |
アカウント | cn=admin,dc=hoge,dc=local |
パスワード | changeme |
ベースDN | ou=Users,dc=hoge,dc=local |
LDAPフィルタ | 空欄 |
タイムアウト | 空欄 |
あわせてユーザを作成 | チェックする |
ログインIDの属性 | cn |
名の属性 | givenName (任意) |
性の属性 | sn (任意) |
メールアドレスの属性 | mail (任意) |
名、性、メールアドレスは、省略するとログイン時に入力を求められる。結局必須属性なので、上記で設定しておいたほうがいい。ただ、名、性は漢字だとInternal errorになった。メールアドレスの形式(xxx@xxxx.xxx
)も厳密にチェックされる。
Rocket.Chat
Rocket.Chat の公式のdocker-compose.ymlだと、mongodbの起動待ちに間に合わず起動しないことが多かったのでwait-for-itで待ち合わせを行うようにした。
しかし、Rocket.ChatのDockerイメージはPORTを環境変数に設定しており、wait-for-it.shは内部でPORT変数を接続確認先のmongodbのポートで上書きする。結果起動はできるが接続できないことになってしまう。仕方ないので、起動時にPORT=3000を再設定している。
さらにそれでも待ち合わせが失敗することがあるようなので、これは単純に、 command: bash -c 'sleep 5 && node main.js'
の方がいいかもしれない。
From rocketchat/rocket.chat:latest
RUN curl -O https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
chmod 755 wait-for-it.sh
version: '2'
services:
rocketchat:
build: .
volumes:
- ./uploads:/app/uploads
depends_on:
- mongo
ports:
- 8086:3000
environment:
- ROOT_URL=http://localhost:8086
command: bash ./wait-for-it.sh -t 0 -s mongo:27017 -- bash -c 'PORT=3000 exec node main.js'
mongo:
image: mongo:latest
volumes:
- ./data/db:/data/db
- ./data/dump:/dump
command: mongod --smallfiles --oplogSize 128 --replSet rs0
mongo-init-replica:
image: mongo:latest
command: 'mongo mongo/rocketchat --eval "rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: ''localhost:27017'' } ]})"'
depends_on:
- mongo
docker-compose up
起動したら http://localhost:8086 に接続し、ユーザ登録を行う。最初に登録したユーザが管理者になる。(あるいは、https://rocket.chat/docs/administrator-guides/create-the-first-admin/ に管理者を登録する方法がある。)
管理者でログイン後、メニューの管理からLDAPを開く。(http://localhost:8086/admin/LDAP でも可)
項目 | 設定値 |
---|---|
有効にする | はい |
Login Fallback | はい |
ホスト | openLDAPを起動したホスト側のIPアドレス |
ポート | 389 |
Reconnect | いいえ |
暗号化 | 暗号化なし |
CA証明書 | 暗号化ありの場合のみ設定可 |
認証できなければ拒否する | 暗号化ありの場合のみ設定可 |
Base DN | ou=Users,dc=hoge,dc=local |
Internal Log Level | Disabled |
上記の状態で一旦設定を保存して、接続テストを行う。
Authentication
項目 | 設定値 |
---|---|
Enable | はい |
User DN | cn=admin,dc=hoge,dc=local |
Password | changeme |
Sync/Import
Rocket.Chatではユーザの名前について以下の設定がある。
設定 | 意味 | 既定の対応するLDAP属性 | この記事での設定例 |
---|---|---|---|
Search Field | ログイン時のID | sAMAccountName | cn |
ユーザ名 |
@xxx の xxx部分 |
sAMAccountName | cn |
名前 | ユーザ情報を参照したときに表示される名前。フルネームなど | ログイン時に入力。「データを同期する」を選んだ場合はcn | displayName |
sAMAccountName
はActiveDirectory向けの属性。
また、注意点としてメールアドレスの設定がないLDAPユーザでログインを行おうとすると、「ユーザーが見付からないか、パスワードが間違っています」というエラーでログインできない。
項目 | 設定値 | 補足 |
---|---|---|
ユーザ名フィールド | cn | 空欄にすると各ユーザがログイン時に入力する(早い者勝ちで任意に選べる)ようになる。 |
一意性を識別するフィールド | objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber | よくわからない。このままでも支障はなさそう |
既定ドメイン | 空欄 | |
Merge Existing Users | いいえ | LDAPなしで既に運用していたなら「はい」にした方がいいかもしれない |
データを同期する | はい | LDAPで一元管理するなら「はい」がいいだろう。各自が名前、メールアドレスをプロフィール設定で書き換えても次のログイン時にLDAPの設定でリセットされる |
ユーザーデータのフィールドマップ | {"displayName":"name", "mail":"email"} | 名前の変更くらいは許可したい場合は{"mail":"email"} とする |
ユーザーのアバターを同期する | はい | LDAPの jpegPhoto 属性でアバターを設定できるらしい #issue 2042 |
Background Sync | いいえ |
User Search
Search Field
以外はデフォルトのままとした。
項目 | 設定値 |
---|---|
Filter | (objectclass=*) |
Scope | sub |
Search Field | cn |
Search Page Size | 250 |
Search Size Limit | 1000 |
Knowledge
Knowledge の場合、デフォルトのアクセス権限(ACL)ではログインできない。
ログインユーザがBaseDN配下を検索できなければならないからである。
そこで、以下のLDIFを作成する。
dn: olcDatabase={1}hdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange
by self write
by dn="cn=admin,dc=hoge,dc=local" write
by anonymous auth
by * none
#olcAccess: {1}to * by * read
olcAccess: {1}to *
by users read
olcAccess: {2}to *
by self write
by dn="cn=admin,dc=hogehoge,dc=local" write
by * none
以下のようにLDAPの設定を修正して ACL を変更する。
ここで、changeme2 は、openldap/docker-compose.yml の LDAP_CONFIG_PASSWORD (ADMINではなくCONFIG)で指定したパスワードである。
$ ldapmodify -D cn=admin,cn=config -w changeme2 -f ldif/acl.ldif
(cn=config、と"cn"であることに注意。ここは間違えやすい。なんだ、cnでも階層を作れるではないか。ここにきて、LDAPのディレクトリ構造はなんでもありで、決めの問題であることを悟った)
コメントで示した以下の例だと誰でも参照できる指定、問題の切り分け等に使う。
olcAccess: {1}to * by * read
以下の例だとusersはログインに成功したユーザを意味し、それに参照権限を与える指定となっている。今回適用したのはこちら。
olcAccess: {1}to * by users read
これら{0}
などの、数字は権限のチェック順序に関係し、{0}
と{2}
は既存の設定である。ここでは、olcAccess: {1}
の設定を挿入したいのでこのように書いてある。
なお、適用した結果は以下のファイルに書かれてある。以下は変更前の内容であり、既存設定はこれを参照した
〜略〜
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by dn="cn
=admin,dc=hoge,dc=local" write by anonymous auth by * none
olcAccess: {1}to * by self write by dn="cn=admin,dc=hoge,dc=local" write by
* none
〜略〜
以下で、起動する。
docker-knowledge:
image: koda/docker-knowledge
volumes:
- ./data:/root/.knowledge
ports:
- 8082:8080
$ docker-compose up
項目 | 値 |
---|---|
URL | http://localhost:8082/ |
デフォルトの管理者ユーザ | admin |
デフォルトのパスワード | admin123 |
右上の人形アイコンからサインインを選び、管理者でログインする。
ログイン後、同じく右上のアイコンからシステム設定を選び、LDAP設定を選ぶ。
追加を押し、以下の設定を行う。
項目 | 設定値 |
---|---|
設定名 | openldap (なんでもいい) |
Host | openLDAPを起動したホスト側のIPアドレス |
Port | 389 |
Security | Plain をチェック |
検索用に接続するためのBind DN | cn=admin,dc=hoge,dc=local |
検索用に接続するためのパスワード | changeme |
ユーザ検索のBaseDN | ou=Users,dc=hoge,dc=local |
ユーザ検索のFilter | (cn=:userid) |
Id Attribute | cn |
Name Attribute | givenName (任意) |
Mail Address Attribute | cn または mail (任意) |
管理者 | admin |
Wekan
Wekan が最近LDAPに対応したのでその設定を示す。
Dockerのイメージは公式の mquandalle/wekanを利用している。
Wekan はデータをmongodbで保持しており、そのフォルダをカレントディレクトリのdata
ディレクトリとしている。
LDAPの設定は、docker-compose.yml 内のLDAP_
で始まる環境変数で設定する。
全ての設定項目は wekan/wekan-ldapや公式のdocker-compose.ymlに記載があるが項目が大変多く混乱しやすい。最低限必要だったのは以下だった。最初は、LDAP_LOG_ENABLED=true
としておいてログ(docker-compose logs -f
)を参照した方が良い。
LDAP_HOST=
はこれまで同様環境に合わせて書き換えること。
公開するポートを 8087 としているが、書き換える箇所が2箇所あるので注意。
LDAP_USERNAME_FIELD
は LDAP_USER_SEARCH_FIELD
と同じであれば必要なさそう(LDAP_USER_SEARCH_FIELD
は必須)。
LDAPユーザにメールアドレスが必須となっている。mailフィールドがないユーザはログインできないが、画面上何もエラーを表示しないためログ(Error: LDAP Authentication succeded, there is no email to create an account.
というメッセージが出る)を見ないとそのことに気づかない。
wekan:
image: mquandalle/wekan
restart: always
links:
- wekandb
environment:
- MONGO_URL=mongodb://wekandb/wekan
- ROOT_URL=http://localhost:8087
# - DEFAULT_AUTHENTICATION_METHOD=ldap
- LDAP_ENABLE=true
- LDAP_PORT=389
- LDAP_HOST=openLDAPを起動したホスト側のIPアドレス
- LDAP_AUTHENTIFICATION=true
- LDAP_AUTHENTIFICATION_USERDN=cn=admin,dc=hoge,dc=local
- LDAP_AUTHENTIFICATION_PASSWORD=changeme
- LDAP_BASEDN=ou=Users,dc=hoge,dc=local
- LDAP_USER_SEARCH_FIELD=cn
#- LDAP_USERNAME_FIELD=cn
- LDAP_FULLNAME_FIELD=displayName
- LDAP_LOG_ENABLED=true
ports:
- 8087:8080
wekandb:
image: mongo
volumes:
- ./data:/data/db
$ docker-compose up
ログインするときは、ログイン画面(以下)の Authentication method
をLDAPに変える必要がある。設定項目の DEFAULT_AUTHENTICATION_METHOD=ldap
は効果がなかったが試行錯誤が足りていない可能性がある。
OpenLDAPコマンド概説
検索
LDAPデータの内容を検索、参照するにはldapsearchコマンドを使う。
$ ldapsearch -D cn=admin,dc=hoge,dc=local -w changeme -b dc=hoge,dc=local
このldapsearchコマンドの結果は以下のようになる。ldapsearchではパスワードや漢字はbase64で、エンコードされた形式で表示される。
# extended LDIF
#
# LDAPv3
# base <dc=hoge,dc=local> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
# hoge.local
dn: dc=hoge,dc=local
objectClass: top
objectClass: dcObject
objectClass: organization
o: HogeHoge Group
dc: hoge
# admin, hoge.local
dn: cn=admin,dc=hoge,dc=local
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
userPassword:: e1NTSEF9QW5JM3pQSDVaT2dnZlR0eHZJOFlRNWJ6a2RaL3VCZ00=
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
最後の numEntries: 2
で検索結果が2件であることを示している。まれに検索結果が失敗して、
# search result
search: 2
result: 32 No such object
# numResponses: 1
という表示を1件ヒットと勘違いしてしまう場合があるので注意が必要である。
ldapsearch コマンドの引数
引数 | 説明 |
---|---|
-L, -LL, -LLL | 出力書式、上の例では使っていないが、Lが多くなるほど単純な出力書式になる。慣れれば、-LLL が見やすい |
-D ユーザ | 接続ユーザ。BindDNと言われることもある。上の例では、初期設定で作成されている管理者ユーザ cn=admin,dc=hoge,dc=local で接続している。このユーザはディレクトリ全体の読み書き権限を持っている |
-w パスワード | -D で指定したユーザのパスワード。これは、docker-compose.yml で指定した環境変数 LDAP_ADMIN_PASSWORD で指定されている。このパスワードは後で変えるべきである |
-H URL | 接続先の指定。デフォルトでは ldap:/// (ローカルホストの389番ポート) を指定したものとなる。この記事では省略する |
-b 検索開始位置 | BaseDNとも言う。上の例では、 dc=hoge,dc=local 配下を検索することを示しており、その配下の階層を検索することを意味する |
フィルタ | 上の例では指定していないが、オプション指定のない最初の引数は、検索結果のデータをフィルタする条件に利用する。デフォルトでは、"(objectclass=*)" を指定したことになる。 |
属性 | これも上の例では指定していないが、オプション指定のない第二引数は、結果を出力する属性を指定する。デフォルトでは、ALL を指定したことになる。 |
-s sub | scope を指定する。後述。 |
-a never | deref を指定する。後述。 |
フィルタと属性を指定した例を示す。
-
フィルタ指定
$ ldapsearch -LLL -D cn=admin,dc=hoge,dc=local -w changeme -b dc=hoge,dc=local "(cn=admin)"
dn: cn=admin,dc=hoge,dc=local
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
userPassword:: e1NTSEF9QW5JM3pQSDVaT2dnZlR0eHZJOFlRNWJ6a2RaL3VCZ00=
```
-
属性指定
$ ldapsearch -LLL -D cn=admin,dc=hoge,dc=local -w changeme -b dc=hoge,dc=local "(cn=admin)" description
dn: cn=admin,dc=hoge,dc=local
description: LDAP administrator
```
scope
-s
は、検索範囲を示すオプション。以下のいずれかを指定する(デフォルトは-s sub
)。値はopenldapのログに現れる数値 (scope=0などと表示される)。認証がうまく行かない場合にどのような検索が行われているかを調べるのに以下の表が必要になる。
オプション指定 | 意味 | 値 |
---|---|---|
base | BaseDNのみ | 0 |
one | BaseDNとその直下のみ | 1 |
sub | BaseDN配下すべて | 2 |
children | BaseDNを除く、その配下すべて | 3 |
deref
-a
は、エイリアスの解釈方法を指定するオプション。以下のいずれかを指定する(デフォルトは-a never
)。-s
同様に、openldapのログにderef=0などと表示される。
オプション指定 | 意味 | 値 |
---|---|---|
never | 常に、エイリアスを解決しない | 0 |
search | 検索対象のみ、エイリアス解決する | 1 |
find | BaseDNのみエイリアス解決する | 2 |
always | 常に、エイリアス解決する | 3 |
上記の意味はマニュアルの字面から解釈したが試してはいない。
追加
$ ldapadd -D cn=admin,dc=hoge,dc=local -w changeme -f ldif/cn-userA.ldif
-f以外は、ldapsearchと同じ。もちろん、標準入力(キーボード入力)を使ってもよいがあまりやらないだろう。
パスワードはハッシュ値を設定する必要があるので実運用では最後に示すようなスクリプトを作るのが良いだろう。
修正
例えば、userA の sn を修正したいとする。以下のようにする
$ ldapmodify -D cn=admin,dc=hoge,dc=local -w changeme
〜以下、キーボードからの入力〜
dn: cn=userA,ou=Users,dc=hoge,dc=local
replace: sn
sn: fugafuga
^D
修正は追加とは書式が異なるのでLDIFファイルを作らず、標準入力から入力する方法を示している。
しかし、どうも指定が冗長である。replace: sn
と書いたら sn 以外のことを書くことはできない。だったら、replace: sn
という行はいらないと思うのだが。
修正したい場合は、LDIFファイルの修正の後、削除と追加をやった方が簡単(階層上、配下のエントリを持つ時にはldapmodifyで頑張るしかない)
削除
$ echo "cn=userA,ou=Users,dc=hoge,dc=local" | ldapdelete -D cn=admin,dc=hoge,dc=local -w changeme
ldapdelete 実行時に "dn: " の文字が必要ないことに注意。全く使いにくい。
追加した時のLDIFファイルを使って
$ head -1 ldif/cn-userA.ldif | cut -c4- | ldapdelete -D cn=admin,dc=hoge,dc=local -w changeme
などとすると確実。
ユーザ追加、修正、削除スクリプトの例
ユーザを追加、削除するシェルスクリプトの例
#!/bin/bash
admin='cn=admin,dc=hoge,dc=local'
adminpass='changeme'
basedn='ou=Users,dc=hoge,dc=local'
usage() {
echo "usage:"
echo " $0 [--] userid [userid2...]" >&2
echo " $0 search [userid...]" >&2
echo " $0 add userid displayName sn mail userPassword" >&2
echo " $0 del userid" >&2
echo " $0 mod userid xxx=xxx [xxx=xxx...]" >&2
exit 2
}
search() {
if [ $# -eq 0 ]; then
ldapsearch -LLL -D $admin -w $adminpass -b $basedn "(cn=*)"
else
for userid; do
ldapsearch -LLL -D $admin -w $adminpass -b $basedn "(cn=$userid)"
done
fi
}
add() {
if [ $# -lt 5 ]; then
usage
fi
userid="$1"; shift
displayName="$1"; shift
sn="$1"; shift
mail="$1"; shift
userPassword="$1"; shift
pass=$(slappasswd -h '{MD5}' -s "$userPassword")
cat <<END | ldapadd -D $admin -w $adminpass
dn: cn=$userid,$basedn
objectClass: inetOrgPerson
displayName: $displayName
sn: $sn
mail: $mail
userPassword: $pass
END
search $userid
}
del() {
if [ $# -lt 1 ]; then
usage
fi
userid="$1"; shift
echo "cn=$userid,$basedn" | ldapdelete -D $admin -w $adminpass
search $userid
}
mod2() {
replacement="$1"; shift
case $replacement in
*=*)
IFS_sv="$IFS" IFS=$'='
set -- $(echo "$replacement")
IFS="$IFS_sv"
id="$1" val="$2"
;;
*) usage;;
esac
cat <<END | ldapmodify -D $admin -w $adminpass
dn: cn=$userid,$basedn
replace: $id
$id: $val
END
}
mod() {
if [ $# -lt 1 ]; then
usage
fi
userid="$1"; shift
for replacement in "$@"
do
mod2 "$replacement"
done
search $userid
}
if [ $# -eq 0 ]; then
usage
fi
opt="$1"; shift
case "$opt" in
add) add ${1+"$@"} ;;
del) del ${1+"$@"} ;;
mod) mod ${1+"$@"} ;;
search|--) search "$@" ;;
*) search "$opt" ${1+"$@"} ;;
esac