Edited at

OpenLDAPで社内サービスのユーザ情報を一元管理する

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


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 ディレクトリに以下のファイルを作成する。


ldif/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 に貼り付ける。面倒なので以降この記事で設定するパスワードはこれを利用する。


ldif/cn-userA.ldif

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のみを持ったユーザの例


ldif/cn-userB.ldif

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を持っている。


ldif/cn-userC.ldif

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


ldif/cn-userD.ldif

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

GitBucket


docker-compose.yml

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の場合、複数モジュールを活用しなければならないので順を追って確認していく。

まず、以下のファイルを作成し、認証のない単純な構成を作る。


docker-compose.yml

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からコメントを削り、必要最低限に絞り込んだ以下を例に使用する。


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というディレクトリを最初に消しに行くので注意。


setup-svnrepo.sh

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になるようだ。


Dockerfile

FROM httpd:alpine

RUN apk --no-cache add mod_dav_svn subversion



docker-compose.yml

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モジュールのパスが違うのはそのためである。


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 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

Redmine


docker-compose.yml

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

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' の方がいいかもしれない。


Dockerfile

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



docker-compose.yml

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を作成する。


ldif/acl.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}の設定を挿入したいのでこのように書いてある。

なお、適用した結果は以下のファイルに書かれてある。以下は変更前の内容であり、既存設定はこれを参照した


slapd.d/cn=config/olcDatabase={1}hdb.ldif

〜略〜

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-compose.yml

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_FIELDLDAP_USER_SEARCH_FIELDと同じであれば必要なさそう(LDAP_USER_SEARCH_FIELDは必須)。

LDAPユーザにメールアドレスが必須となっている。mailフィールドがないユーザはログインできないが、画面上何もエラーを表示しないためログ(Error: LDAP Authentication succeded, there is no email to create an account.というメッセージが出る)を見ないとそのことに気づかない。


docker-compose.yml

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