背景
OpenLDAPのback_metaモジュールを利用して、複数のLDAPサーバーを一つのディレクトリ・ツリーに集約するコンテナをKubernetes上で稼動させています。
独自のLDAPサーバーにはメーリングリストのアドレスとメンバー情報をまとめたディレクトリーにしていて、ou=mailgroupとしています。これと組織のou=peopleを組み合わせてWebサーバーやOpenIDのグループ認証によるアクセス・コントロールに利用しています。
これまでUbuntuコンテナを利用していましたが、より軽量なAlpineコンテナを利用しようと切り替え作業に取り掛かったところ、単純な置き換えだけではどうしてもback_metaモジュールが構成できなくなりました。
OpenLDAPのバージョンの違いなどが主な原因かなと思ったのですが、根本原因は違ったので顛末をまとめておきます。
back_metaモジュールは強力だとは思うのですが、なにしろ資料が少ないので使う際の参考にもなれば幸いです。
OpenLDAPパッケージのバージョン
ベースとなるコンテナとOpenLDAPパッケージのバージョンは次のとおりです。
最初はバージョン間の挙動が原因かと思ったので、最近のAlpineは一通り試しています。
Distribution | OpenLDAPパッケージ |
---|---|
Ubuntu 22.04 | 2.5.15 (LTS) |
Alpine 3.14 | 2.4.58 |
Alpine 3.15 | 2.6.2 |
Alpine 3.18 | 2.6.5 |
Alpine edge | 2.6.6 |
最近のリリースノートをみると2.6系列ではback_metaモジュールについては修正がほぼ毎回含まれているので、できるだけ最新のバージョンを選択した方が良いでしょう。
2.5系列にもほぼ同様のバックポートが行われているようなので、LTS版であることとv2.7リリースが想定されていることなどを考えるとalpineで2.5系列が利用可能であれば選択する余地はありそうです。
参考資料
Ubuntu版の基本的な構成
Ubuntu版では次のような設定を追加しています。
## 01.module.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: back_ldap
-
add: olcModuleLoad
olcModuleLoad: back_meta
-
add: olcModuleLoad
olcModuleLoad: rwm
-
add: olcModuleLoad
olcModuleLoad: pcache
-
add: olcModuleLoad
olcModuleLoad: ppolicy
## 02.meta.ldif
dn: olcDatabase=meta,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMetaConfig
olcDatabase: meta
olcSuffix: ou=proxy,dc=example,dc=com
olcAccess: to * by * read
dn: olcMetaSub={0}uri,olcDatabase={1}meta,cn=config
objectClass: olcMetaTargetConfig
olcMetaSub: {0}uri
olcDbURI: ldaps://ldap.example.com/ou=people,ou=proxy,dc=example,dc=com
olcDbRewrite: {0}suffixmassage "ou=people,ou=proxy,dc=example,dc=com" "ou=people,dc=example,dc=com"
dn: olcMetaSub={1}uri,olcDatabase={1}meta,cn=config
objectClass: olcMetaTargetConfig
olcMetaSub: {1}uri
olcDbURI: ldaps://ldap.example.com/ou=group,ou=proxy,dc=example,dc=com
olcDbRewrite: {0}suffixmassage "ou=group,ou=proxy,dc=example,dc=com" "ou=group,dc=example,dc=com"
テスト環境と本番環境では接続先のサーバーが異なる事と、セキュリティ上の都合で実サーバー情報をコンテナの中に入れない事から、実行時に環境変数で外部から接続先のサーバーやDN等を変更できるようにしています。
Ubuntu版では実行時にldapadd -H ldapi:/// -Y EXTERNAL
を利用して追加で構成を追加していました。
これをAlpine版に移行していきます。
Ubuntu版の動作概要
Dockerfile (Ubuntu版)
Dockerfile側ではアプリケーションの導入のみを行い、コンテナ実行開始時には何も設定されていないインストール後の初期状態となっています。
FROM ubuntu:jammy-20230624
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends slapd ldap-utils ca-certificates
ENV ...
COPY ldif /ldif
COPY run.sh /run.sh
RUN chmod +x /run.sh
EXPOSE 389
ENTRYPOINT ["/run.sh"]
特にセキュリティ面への配慮などは行っておらず実行時にはroot権限で動作し、389ポートを使用しています。
run.sh(Ubuntu版)
ENTRYPOINTに指定しているスクリプトは概ね、次のような動作をしています。
#!/bin/bash -x
TMPLDIF="/ldif/tmp.$$.ldif"
rm -rf '/etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb.ldif'
/usr/sbin/slapd -d 0 -h "ldap://localhost:1389/ ldapi:///" -F /etc/ldap/slapd.d &
sleep 2
for file in /ldif/metaldap_*.ldif
do
sed -i \
-e "s/LDAP_PEOPLE_HOST/$LDAP_PEOPLE_HOST/g" \
-e ... \
"${file}"
ldapadd -v -Q -Y EXTERNAL -H ldapi:/// -f "${file}"
done
killall slapd
sleep 2
exec /usr/sbin/slapd -d 0 -h "ldap:///" -F /etc/ldap/slapd.d
LDIFファイルは前述したような内容になっており、/ldif以下に配置されています。
Alpine版で遭遇したエラー
Ubuntu版とAlpine版では、ディレクトリ構造が/etc/ldap/と/etc/openldap/のように違うだけで基本的には同じコードが動作することを期待しました。
今回は使用しませんが、各バックエンドのファイル配置場所としては、Ubuntu版では/var/lib/ldap/が、Alpine版では/var/lib/openldap/が想定されていますが、これは設定で変更可能なため無理に書き換える必要はありません。
単純に書き換えてコンテナを動作させると、Alpine版では次のようなメッセージが表示されます。
+ /usr/sbin/slapd -d -1 -h ldapi:/// -F /etc/openldap/slapd.d
651222db.32b275b8 0x7f168ae7cb48 @(#) $OpenLDAP: slapd 2.6.5 (Jul 11 2023 03:35:58) $
...
+ ldapadd -v -Q -Y EXTERNAL -H ldapi:/// -f /ldif/metaldap_02_config_metabackend.ldif
ldap_initialize( ldapi:///??base )
...
651222de.330a24fe 0x7f161c939b38 => acl_mask: to all values by "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth", (=0)
651222de.330a3501 0x7f161c939b38 <= check a_dn_pat: *
651222de.330a590d 0x7f161c939b38 <= acl_mask: [1] applying none(=0) (stop)
651222de.330a6bf0 0x7f161c939b38 <= acl_mask: [1] mask: none(=0)
651222de.330a8404 0x7f161c939b38 => slap_access_allowed: add access denied by none(=0)
651222de.330a9696 0x7f161c939b38 => access_allowed: no more rules
Assertion failed: pool->ltp_pause == PAUSED (tpool.c: ldap_pvt_thread_pool_resume: 1319)
ldap_result: Can't contact LDAP server (-1)
/run.sh: line 7: 7 Aborted (core dumped) /usr/sbin/slapd -d -1 -u ldap -h "ldap://0.0.0.0:389 ldapi:///" -F /etc/openldap/slapd.d
このメッセージはalpine:edgeのOpenLDAP v2.6.6パッケージを利用すると少し変化し、core dumpすることはなくなります。
いずれにしてもこれは正常に動作していません。最初はメッセージをちゃんと読めていなかったのですが、抜粋したところにあるように権限チェックに失敗し、アクセスを拒否されています。
この点を修正すると無事に動作するようになります。
## 02.meta.ldif
dn: olcDatabase=meta,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMetaConfig
olcDatabase: meta
olcSuffix: ou=proxy,dc=example,dc=com
olcAccess: to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
olcAccess: to * by dn.base="gidNumber=101+uidNumber=100,cn=peercred,cn=external,cn=auth" write
olcAccess: to * by * read
olcAccessにはroot権限(uid,gid)==(0,0)でのアクセスと、ldapユーザー(uid,gid)==(100,101)のアクセスに両方対応できるように設定を追加していますが、必要に応じて片側は削除しても問題ありません。
ldapi:///経由でのアクセスはUNIX socketにアクセスできた時点で認証されていると思っていたので、この動作は想定外でした。
最終的に選択したコンテナの構成方法
結局、設定を変更したいのは環境変数を読み取った起動時のタイミングだけだったため、olcAccessを加えるのではなく、必要な設定を全て/etc/openldap/slapd.ldifに加え、ldapaddコマンドによって、/etc/openldap/slapd.d/ディレクトリを作成する方針に変更しています。
ENTRYPOINTに指定しているrun.shスクリプトからslapadd -n 0 -F /etc/openldap/slapd.d -l /etc/openldap/slapd.ldif
を実行するように変更しています。
初期状態の/etc/openldap/slapd.ldifファイルに全てのランタイムでの変更を反映させることにしたため、ldapi:///は利用することがなくなりました。
Dockerfile (Alpine版)
参考資料に挙げたようにAlpineのWikiに従って構成しています。
FROM alpine:3.18.3
RUN apk add --no-cache tzdata bash sed ca-certificates openldap openldap-clients \
openldap-back-ldap \
openldap-back-meta \
openldap-overlay-ppolicy \
openldap-overlay-proxycache \
openldap-overlay-rwm
## Reference: https://wiki.alpinelinux.org/wiki/Configure_OpenLDAP
RUN rm -f /etc/openldap/slapd.conf
RUN install -m 755 -o ldap -g ldap -d /etc/openldap/slapd.d
ADD files/slapd.ldif /etc/openldap/slapd.ldif
RUN chown ldap:ldap /etc/openldap/slapd.ldif
RUN install -m 755 -o ldap -g ldap -d /var/lib/openldap/run
RUN chown -R ldap:ldap /etc/openldap/slapd.d
COPY ldif /ldif
COPY run.sh /run.sh
RUN chmod 0755 /run.sh
ENV ...
EXPOSE 1389
USER ldap
ENTRYPOINT ["/run.sh"]
run.sh (Alpine版)
動的に構成するためslapadd -n 0
コマンドの実行をrun.sh内部に持ってきています。
#!/bin/bash -x
for file in /ldif/back_meta_*.ldif
do
## LDIFでは空行が境界を表すため、複数の設定を確実に分離するために空行を挿入する
echo "" >> /etc/openldap/slapd.ldif
sed \
-e "s/dc=example,dc=com/${TARGET_BASEDN}" \
"${file}" >> /etc/openldap/slapd.ldif
## LDIFでは空行が境界を表すため、複数の設定を確実に分離するために空行を挿入する
echo "" >> /etc/openldap/slapd.ldif
done
slapadd -n 0 -F /etc/openldap/slapd.d -l /etc/openldap/slapd.ldif
exec /usr/sbin/slapd -u ldap -d 0 -h "ldap://0.0.0.0:1389/" -F /etc/openldap/slapd.d
実際にはsedでは多くの環境変数を参照し、設定ファイルを生成しています。
slapd.ldif (Alpine版)
files/slapd.ldif はパッケージ付属のslapd.ldifを元にback_meta.soモジュールの追加などを行っています。
dn: cn=config
objectClass: olcGlobal
cn: config
dn: cn=module,cn=config
objectClass: olcModuleList
cn: module
olcModulepath: /usr/lib/openldap
olcModuleload: back_ldap.so
olcModuleload: back_meta.so
olcModuleload: rwm.so
olcModuleload: pcache.so
olcModuleload: ppolicy.so
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
include: file:///etc/openldap/schema/core.ldif
include: file:///etc/openldap/schema/cosine.ldif
include: file:///etc/openldap/schema/inetorgperson.ldif
include: file:///etc/openldap/schema/nis.ldif
dn: olcDatabase=frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: frontend
結果的にolcAccessに対する変更は行なわずに実行時にはback_metaだけが動作するようになっています。
Alpineに変更した効果
動作していれば無理にコンテナを変更する必要はないという考えもありますが、メンテナンスも技術なので定期的に実施していかないと様々な変化に対応できません。動作の安定など新しいソフトウェアに期待することもあります。
DockerHubでコンテナのサイズを確認すると、Ubuntu版では約70MBだったファイルサイズがAlpine版では約7MBにまで少なくなっています。
ベースのコンテナを含めると約180MBだったサイズが17MB程度まで削減されているので、それなりの効果はあったかなと思います。
ここまでの振り返り
LDAPはNISが消滅したいまとなっては、非常に強力なユーザー・ディレクトリを提供するための手段となっています。
上位組織が提供するディレクトリ・サービスに独自のツリーを追加できるback_metaモジュールはとても便利だと思うのですが、何しろ具体的な資料がほとんど見つかりません。
この資料が何かしらの参考になれば幸いです。
この他の課題 (Syncprovサーバーの場合)
他にもいろいろsyncprovを利用しているOpenLDAPサーバーなどがあるので、Alpineに変更する際に遭遇した課題についてまとめておきます。
database #0 (cn=config) not configured to hold "...."
他のOpenLDAPコンテナもAlpineに変更しようとしましたが、slapadd -n 0
はcn=configを変更するためのもので、個別のolcDatabase={1}mdb,cn=configの中を設定することはできません。
slapadd: line 1079: database #0 (cn=config) not configured to hold "dc=example,dc=com"; did you mean to use database #1 (dc=example,dc=com)?
Closing DB...
具体的にはdcObjectの設定などのディレクトリ・ツリーに対する操作をslapd.ldifに混ぜると、このメッセージが表示されます。
このためldapi:///を利用しなければいけない状況も引き続き存在していて、コンテナの構成を少し工夫する必要がありました。
Ubuntu版Syncprovサーバーの構成
このコンテナはsyncprovを使用している以外はback_mdbを使用している普通(?)のLDAPサーバーです。
動作しているUbuntu版ではback_metaが動作するサーバーと同様にDockerfileでパッケージの導入までを実施し、ENTRYPOINTに指定しているrun.sh内部で様々な設定を行っています。
run.sh (Ubuntu版)
Ubuntu版のslapdパッケージは導入した時点で初期DBが作成されます。
オプションを変更してこれを構成しても良いのですが、最初にmdbファイルを削除しています。
#!/bin/bash -x
rm -fr '/etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb'
rm -f '/etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb.ldif'
/usr/sbin/slapd -d 32768 -h "ldap://127.0.0.1:1389/ ldapi:///" -F /etc/ldap/slapd.d &
sleep 2
for file in /ldif/ldap_0[12345]*.ldif
do
sed -i \
-e ...
"${file}"
done
for file in /ldif/ldap_*.ldif
do
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "${file}"
done
## add baseDN
ldapadd -x -h 127.0.0.1 -p 1389 -D cn=admin,"${YALDAP_BASEDN}" -w "${YALDAP_ROOTPW}" -f ldif/ldap_03_add_subentries.ldif
killall slapd
sleep 2
( sleep 10 && /run-update-data.sh ) &
exec /usr/sbin/slapd -d 32768 -h ldap:/// -F /etc/ldap/slapd.d
ldapaddコマンドは現在では**-hや-pオプションは廃止されて-H**によるURL表記に統一されているため、このままでは新しいUbuntuのコンテナでも動作しません。
このコードでは全ての変更をldapaddを利用して、**ldapi:///**インタフェースを利用して実施していました。
ここで反映していたLDIFファイルは次のような内容です。
LDIFファイル(Ubuntu版)
## ldap_01_add_modules.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: back_ldap
-
add: olcModuleLoad
olcModuleLoad: pcache
-
add: olcModuleLoad
olcModuleLoad: ppolicy
-
add: olcModuleLoad
olcModuleLoad: syncprov
## ldap_02_create_backenddb.ldif
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
olcDbMaxSize: 1073741824
olcSuffix: dc=example,dc=com
olcRootDN: cn=admin,dc=example,dc=com
olcRootPW: {SSHA}7LWYlCr2Rr7k6ihxJnb8ao65pN9GhcRF
olcDbDirectory: /var/lib/ldap/mailgroup
olcDbIndex: objectClass,entryCSN,entryUUID eq
olcDbIndex: default eq,pres
olcDbIndex: uid
olcDbIndex: cn
olcDbIndex: member
olcAccess: to * by * read
dn: olcOverlay=syncprov,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov
olcSpSessionLog: 100
olcSpCheckpoint: 100 10
## ldap_03_add_subentries.ldif
dn: dc=example,dc=com
objectClass: dcObject
objectclass: organization
dc: example
o: example
dn: ou=mailgroup,dc=example,dc=com
objectclass: organizationalUnit
ou: mailgroup
## ldap_04_add_pcacheConfig.ldif
dn: olcOverlay=pcache,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPcacheConfig
olcOverlay: pcache
olcPcache: mdb 100000 1 1000 100
olcPcacheAttrset: 0 *
## ldap_05_add_pcache_backenddb.ldif
dn: olcDatabase=mdb,olcOverlay={1}pcache,olcDatabase={1}mdb,cn=config
objectClass: olcMdbConfig
objectClass: olcPcacheDatabase
olcDatabase: mdb
olcDbDirectory: /var/lib/ldap/pcache
olcDbIndex: objectClass eq
olcDbIndex: cn pres,eq,sub
olcDbIndex: pcacheQueryID eq
olcAccess: to * by * read
これらをそのままAlpine版に移行すること難しそうだったので、概ねback_metaモジュールの時と同様に対応します。
ただslapadd -n 0
コマンドで対応できない "dn: dc=example,dc=com" と "dn: ou=mailgroup,dc=example,dc=com" についての設定は別のLDIFファイルに保存しておきます。
LDIFファイル(Alpine版)
基本的には同様ですが、olcDatabase={1}mdb,cn=configを設定しているところはolcAccessの部分が変更になっています。
dn: olcDatabase={1}mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbMaxSize: 1073741824
olcSuffix: dc=example,dc=com
olcRootDN: cn=admin,dc=example,dc=com
olcRootPW: {SSHA}7LWYlCr2Rr7k6ihxJnb8ao65pN9GhcRF
olcDbDirectory: /var/lib/openldap/mailgroup
olcDbIndex: objectClass,entryCSN,entryUUID eq
olcDbIndex: default eq,pres
olcDbIndex: uid
olcDbIndex: cn
olcDbIndex: member
olcAccess: to *
by dn.base="gidNumber=101+uidNumber=100,cn=peercred,cn=external,cn=auth" manage
by * read
細かい点ではolcDbDirectoryも変更になっていますが、ここはAlpine版パッケージの形式に合わせただけです。
back_metaではldapaddコマンドを使いませんでしたが、ここで利用する機会が出てきました。
これで別ファイルに作成したLDIFファイルがldapaddで反映できます。
## ldap_03_add_subentries.ldif
dn: dc=example,dc=com
objectClass: dcObject
objectclass: organization
dc: example
o: example
dn: ou=mailgroup,dc=example,dc=com
objectclass: organizationalUnit
ou: mailgroup
$ ldapadd -H "ldapi:///" -Y EXTERNAL -f ldap_03_add_subentries.ldif
どうして今までrootユーザーで操作できていたのか
Ubuntuでは特別に苦労なくrootユーザーでldapi:///にアクセスすれば変更が全て反映できていました。
Alpineに変更するとrootユーザーであってもldapi:///経由での変更要求が権限不足で却下されるようになって、いろいろな回避策を取ってきました。
ここまでやってきてようやく、slapadd -n 0
でslapd.ldifをslapd.dに変換する時に、何か必要な情報を加えればolcDatabase={0}config,cn=config
やcn=module{0}
を、後からldapaddやldapmodifyで操作できるのだろうということはなんとなく分かってきました。
Ubuntuのslapdパッケージを確認する
まずはパッケージのコード全体を取得して、cn=externalをキーワードにgrepをかけてみます。
$ cd /tmp
$ apt source slapd
$ cd openldap-2.5.16+dfsg/debian/
$ grep cn=external *
slapd.init.ldif:olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
slapd.init.ldif:olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
slapd.scripts-common:olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break\
slapd.scripts-common:olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break' "${SLA
PD_CONF}/cn=config/olcDatabase={0}config.ldif"
debパッケージを作成する時のマナーでオリジナルとの変更点は全てdebian/ディレクトリにまとめられているため、ここでgrepをかけてみました。
slapd.init.ldifがそれっぽいので内容を確認してみます。
# Global config:
dn: cn=config
objectClass: olcGlobal
cn: config
# Where the pid file is put. The init.d script
# will not stop the server if you change this.
olcPidFile: /var/run/slapd/slapd.pid
# List of arguments that were passed to the server
olcArgsFile: /var/run/slapd/slapd.args
# Read slapd-config(5) for possible values
olcLogLevel: none
# The tool-threads parameter sets the actual amount of cpu's that is used
# for indexing.
olcToolThreads: 1
# Frontend settings
dn: olcDatabase={-1}frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: {-1}frontend
# The maximum number of entries that is returned for a search operation
olcSizeLimit: 500
# Allow unlimited access to local connection from the local root user
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
# Allow unauthenticated read access for schema and base DN autodiscovery
olcAccess: {1}to dn.exact="" by * read
olcAccess: {2}to dn.base="cn=Subschema" by * read
# Config db settings
dn: olcDatabase=config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: config
# Allow unlimited access to local connection from the local root user
olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcRootDN: cn=admin,cn=config
Alpine付属のslapd.iniのようにolcGlobal関連の設定から始まりますが、その次のolcDatabase={-1}frontend,cn=configや、olcDatabase=config,cn=configのような設定にはolcAccessの指定としてdn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
の指定がばっちりと入ってきます。
この設定だとroot以外のユーザーでslapdを動作させるというのは少し手間がかかりそうです。
必要であればこういった設定をslapd.ldifに加えてslapadd -n 0
で反映させれば良いのでしょうけれど、Ubuntuでは手が入っていて可能だったという理由が分かったのでこれで良しとします。
さいごに
レプリケーションやback_metaモジュールを利用している範囲では、Alpineを利用しなければいけないということはないと思います。
ただUbuntuは手軽に利用できるよう様々な調整がされていることを考えると、慣れてくると細かい制御がしやすいという点ではAlpineにも利点はあると思います。
あまりOpenLDAPに慣れていなければUbuntuでまずは利用を始めるのが資料も多くて良いのではないかと思います。
サービスとしてコンテナにまとめるにはAlpine版を検討すると良いと思いますが、手間の割には得られる利点はそれほどないかもしれません。
個人的には改めてOpenLDAPに触れる良い機会でしたし、またいくらか構成をシンプルに出来たので満足しています。