ansible-ldap is very simple and useful

OpenLDAP と仲間たち Advent Calendar 2015 24日目、クリスマスイブですね。一昨日(12/22)にEngineer All Handsという社内のイベントでLTをすることになり、「LDAPと私」というネタで ansible-ldap というモジュールの話を軽くしました。ついでにブログにでもちゃんと書いておこうかなと思い、昨日アドベントカレンダーの予定を見てみたら、空いていたので参加してみました。

概要

この記事の要点としては以下のとおりです。これを読んで理解できる方はその後を読む必要はありません。モジュールのソースコード内のドキュメントを読みましょう。

  • ansible-ldapの ldap_entry および ldap_attr モジュールだけで LDAPのエントリーの追加・削除、エントリーの属性の追加、削除、置換を冪等に行うことができる

  • ldapi:// にも対応しているので slapd-config(5)の設定もできる

  • Ansible Galaxy には登録されてないのでパッチ書いてPR送った (mergeされるかは別の話)

あと、LDAPには直接関係ありませんが、

ansible-ldapとは

django-otpdjango-auth-ldapmockldap などの作者の Peter Sagerson 氏が開発された Ansib用の LDAP モジュールです。django-auth-ldapやmockldapには前職でとてもお世話になってました。 1

ldap_entryldap_attr の2つのモジュールがあります。 前者はLDAPのDITに対し、entryを追加または削除を、後者はすでに存在するエントリーの属性を追加、削除、もしくは置き換えを行うためのモジュールです。

fumiyasuさん が一日目にslapd.conf(5)を使ったPlaybookの 記事 を書かれていますが、ansible-ldapモジュールを使う場合は、slapd-config(5)を使います。DebianやUbuntuのOpenLDAPサーバのパッケージである、slapdパッケージはslapd-config(5)がデフォルトです。slapd-config(5)絡みの記事は 以前書いたものある ので、そちらもご参照ください。

使い方

これらのモジュールを使った サンプルのPlaybook を作ったのでそれを例にして説明します。

ldap_entry

まずはシンプルに memberof モジュールを追加する例です。

- name: be sure memberof module
  ldap_entry:
    dn: cn=module{1},cn=config
    state: present
    objectClass: olcModuleList
    olcModulePath: /usr/lib/ldap
    olcModuleLoad: memberof.la
dn

必須項目です。エントリーの追加時、 {1} とあるindex番号は必要に応じて自動的に付加されます。しかし、 cn=module,cn=configcn=module{1},cn=config は別のdnになります。そのためエントリー追加後、同様にindexを指定しないで属性の変更や、エントリーの削除を行おうとすると、エントリーが見つからないためエラーになります。変更するエントリーのindexを把握しておく必要があるのでサンプルで明示的に指定しています。

state

デフォルトは present で存在しなければ作成し、存在すれば何もしません。削除するときは absent を使います。これはAnsibleの他モジュールと同じ挙動です。 ldap_entry には変更という操作はありません。つまり ldapadd コマンドと ldapdelete コマンドに相当する操作だけです。 ldapmodify コマンドに相当する操作は ldap_attr で行います。

objectClass 及びその他の属性

必要な場合は設定します。 slapd-config(5)の属性の名称はslapd.conf(5) の設定オプション名とは微妙に異なるので、man slapd-config(5)GLOBAL CONFIGURATION OPTIONS 以降のセクションを参照しましょう。

次に、suffixが dc=example,dc=org のLDAPディレクトリに対し、oganizational unitを追加する例を見てみます。

- ldap_entry:
    dn: "ou={{ item }},{{ suffix }}"
    objectClass: organizationalUnit
    ou: "{{ item }}"
    bind_dn: "cn=admin,{{ suffix }}"
    bind_pw: "{{ admin_pw }}"
    state: present
  with_items:
    - People
    - Groups
    - SUDOers
server_uri

この例では省略していますが、これはデフォルトでは ldapi:/// になります。リモートホストのLDAPサーバを対象にする場合には、LDAPのURLを指定する必要があります。

start_tls

StartTLS LDAPを使うときはこのオプションを true にします。デフォルトでは false です。

bind_dnbind_pw

slapd-config(5) の設定の時は省略することで EXTERNAL mechanism でアクセスしますが、特定のDITの操作を行うには、デフォルトでは認証が必要になります。bind用のDNを bind_dn で、パスワードを bind_pw で指定します。slapd自体の設定し、特定のsuffix のDITに対し変更を行う場合、つい忘れがちなので気をつけましょう。

ldap_attr

アクセス権限を設定する例を見てみます。

- name: olcAccess are absent.
  ldap_attr:
     dn: "olcDatabase={1}{{ backend | lower }},cn=config"
     name: olcAccess
     state: absent
     values:
       - '{0}to attrs=userPassword by self write by anonymous auth by * none'
       - '{1}to attrs=shadowLastChange by self write by * read'
       - '{2}to * by * read'

- name: olcAccess are present.
   ldap_attr:
     dn: "olcDatabase={1}{{ backend | lower }},cn=config"
     name: olcAccess
     state: present
     values:
       - '{0}to attrs=userPassword,shadowLastChange
          by self write
          by anonymous auth
          by dn="cn=admin,{{ suffix }}" write
          by * none'
       - '{1}to dn.base=""
          by * read'
       - '{2}to *
          by dn="cn=admin,{{ suffix }}" write
          by * read'
       - '{3}to dn.subtree="{{ suffix }}"
          by self read
          by * read'
       - '{4}to *
          by * none'

この2つのタスクでは、 absent でslapdインストール時にデフォルトで設定されるアクセス設定を一度削除し、 present で新しく設定しています。

このやり方は面倒ですね。代わりに exact を使えばひとつのタスクで変更できます。

- name: override olcAccess exactly
  ldap_attr:
    dn: "olcDatabase={1}{{ backend | lower }},cn=config"
    name: olcAccess
    state: exact
    values:
      - '{0}to attrs=userPassword,shadowLastChange
         by self write
         by anonymous auth
         by dn="cn=admin,{{ suffix }}" write
         by * none'
      - '{1}to dn.base=""
         by * read'
      - '{2}to *
         by dn="cn=admin,{{ suffix }}" write
         by * read'
      - '{3}to dn.subtree="{{ suffix }}"
         by self read
         by * read'
      - '{4}to *
         by * none'

name で 変更する属性を指定し、 values で一つもしくは一つ以上の値を指定します。複数設定できるか否かは、設定するattributeのスキーマ次第です。他のパラメータは基本的には ldap_entry と同じです。

Note

backend には MDB を指定しています。MDBは LMDB をバックエンドとするためのdebconfのパラメータです。DNでは小文字になるため、lowerフィルターを使って小文字に変換しています。

ansible-ldapのインストール方法

現状、ansible-ldapは Ansible Galaxyには登録されてません。また、Ansible Galaxyで公開できる形式になっていないため、requirements.yml に

- src: https://bitbucket.org/psagers/ansible-ldap
  name: ldap
  scm: hg

のように記述し、 ansible-galaxy install -p library -r requirements.yml と実行してもインストールできません。手動で hg clone を実行し、playbookのlibraryディレクトリを以下にモジュールをコピーする必要があります。とても面倒です。ということで、パッチ書いてPRを送っておきました。

マージされるまでの間 2 は、下記のように記述することで ansible-galaxy install コマンドでインストールすることができます。ただし、 --no-deps オプションが必要ですので気をつけましょう。

- src: https://bitbucket.org/mkouhei/ansible-ldap
  name: ldap
  scm: hg
  version: for-ansible-galaxy

さらにもしAnsible Galaxyに登録されたら、おそらくこんな記述になることでしょう。

- src: psagers.ldap

C bindingとPure Python

今回紹介した django-auth-ldap、mock-ldapは OpenLDAPライブラリの C bindingと実装された Python-LDAP やそのPython3対応としてのforkの pyldap に依存してします。 ansible-ldapもPython-LDAPに依存しています。 3 Pure PythonでのLDAPクライアントの実装としての ldap3 は使用されていません。今回紹介する ansible-ldap も やはり Python-LDAPに依存しています。

今までに何度かLDAP用のAnsibleモジュールを書こうかな、と思ったことも何度かあったのですが 4 、slapdの設定変更に必要なのは ldapi:// (LDAP over IPC) でアクセス、操作できることなので、少なくともこの10月末まではC bindingのPython-LDAP / pyldapしかその機能があるPythonモジュールはありませんでした。 Pure Pythonのldap3では本当にこの最近(2015-11-15)、 v0.9.9.3 として LDAPIの機能が実装された ようです。

一方、このansible-ldapは 昨年の11月に基本機能を実装して公開されていた いたので、ldap3を使っていないのは当然といえます。

DebianシステムではPython-LDAPは python-ldap パッケージとして提供されていますが、pyldapはDebianパッケージとして提供されていません。ldap3 は python-ldap3 (Python2版) および python3-ldap3 (Python3版) として提供されています。Ansible は Python3はまだ正式対応されていないので現状では playbook の中で、

- apt: pkg=python-ldap state=present

python-ldap パッケージをインストールすればよいですが 5 、Ubuntu の次のLTSではPython3だけになるので、pyenvなどでPython2.7を構築するタスクを書いた上 6

- apt:
    pkg={{ item }}
    state=present
  with_items:
    - build-essential
    - libldap2-dev
  - pip: name=Python-LDAP

として、slapdを動かすホスト上でPython-LDAPのコンパイルも必要な上、ansible-ldapでは現状任意の PYTHONPATH を指定することができないので、 /usr/local/lib/python2.7/dist-packages の下にPython-LDAPをインストール必要があります。

(おまけ) CSVからvarsを読み込むモジュールを作りました

サンプルのPlaybookではユーザーの作成や、SSH公開鍵を登録するための タスク もあるのですが、ユーザー作成用のパラメータや公開鍵をいちいちYAMLで記述するのはとても億劫です。なので、CSVで記述したものをvarsとして読み込むことのできる include_csv というモジュールもついでに作りました。使い方としては、コアモジュールの include_vars のような使い方になります。詳しくはAnsible GalaxyのREADMEのページを参照してください。

まとめ

今までは、Ansibleらしくない書き方でslapdの構築を行い、それが故に冪等にすることが難しいため変更はAnsibleで行わない、という運用になってしまっていましたが、このansible-ldapモジュールのおかげで冪等性を保つことができるようになりました。個人的には ldapvi コマンド、Python-LDAPに続く、LDAPの運用・利用が非常に楽になるツールが登場したと思ってます。勝手に三種の神器と呼びたい。

また、include_csv も便利そうという意見ももらったので結構うれしいですね。 7

footnotes

1

11月からRuby on Railsの仕事をすることになり、業務では現時点ではPythonもLDAPも使っていません。

2

マージされるかはわかりませんが。

3

ansible 2.0.0-0.8.rc3 も試してみましたが、現状ではまだ ansible-galaxy コマンドが Python3 に対応していませんでした。

4

頻度の問題で、結局作らずに済ませてしまってきたのですが…。

5

Debianシステムの場合。

6

今回の話と少しずれるので省略します。

7

ちなみに今回、初Ansible Galaxy、つまり初のAnsible モジュール作成、初のアカウント作成、初のRole登録、初の ansible-galaxy コマンド利用、と初ものづくしでした。

Changed the display manager to slim instead of gdm3

一週間ほど前(2015/10/12)から、stretch/sidでパッケージのアップデートを行うと、次回のOS起動時にDisplay managerが起動せず、ttyのログインプロンプトが延々と明滅してまともにログインすらできない問題に遭遇しました。

私物のMacBook ProのVirtualBox上で動かしているSidは、VirtualBox用のカーネルモジュールを再作成してやることで回避できました。

$ sudo dpkg-reconfigure virtualbox-guest-dkms

しかし、仕事で使っているMacBook ProのVirtualBox上のSid 1 ではこれでは解決せず、recovery modeで/var/log/Xorg.0.logを確認してみたところ、

(EE) dbus-core: error connecting to system bus: org.freedesktop.Dbus.Error.FileNotFound (Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory

というログが出ていたのでパッケージを確認してみたら、 libdbus-c++-10v5 パッケージが無く、これをインストールすると、 startx コマンドでX Window systemが起動するようにはなりました。 2

ところが、これでもgdm3は起動しません。そこで、現在Debianでサポートされている Display manager を確認して、slimに変更してみました。これは正常に起動できました。

デフォルトのセッションがawesomeではなかったので、 /etc/slim.conflogin_cmd

diff --git a/slim.conf b/slim.conf
index c82f73b..413e111 100644
--- a/slim.conf
+++ b/slim.conf
@@ -33,8 +33,9 @@ authfile           /var/run/slim.auth
# NOTE: if your system does not have bash you need
# to adjust the command according to your preferred shell,
# i.e. for freebsd use:
-# login_cmd           exec /bin/sh - ~/.xinitrc %session
-login_cmd           exec /bin/bash -login /etc/X11/Xsession %session
+#login_cmd           exec /bin/sh - ~/.xinitrc %session
+#login_cmd           exec /bin/bash -login /etc/X11/Xsession %session
+login_cmd           exec /bin/bash -login /etc/X11/Xsession awesome

# Commands executed when starting and exiting a session.
# They can be used for registering a X11 session with

と直接指定しています。 3

普段、Gnomeではなくawesomeしか使っていないので、別に gdm3を使う理由も無いので、とりあえずこれで良しとします。問題のなかった私物の環境の方もslimに変更しようかな。

footnotes

1

このSidのイメージはもともと自宅で使っているSidのイメージを使っているのですが、まぁ年月経つと大分環境にも差異が出ますよね。

2

ちなみに xinit パッケージもインストールされていませんでした。Display manager使っていたら普段必要ないのですけど。

3

ちなみに ~/.xinitrcsession=awesome を設定し、 exec /bin/sh - ~/.xinitrc %session を有効にする方法だと、gdmの時と同様の問題が発生してしまいました。

Rewrote the sctipt of OpenSSH AuthorizedKeysCommand for LDAP public key

OpenSSH 6.2以上で実装された AuthorizedKeysCommand を使って、LDAPでの公開鍵認証用のシェルスクリプトを Golang で書き換えました。その理由などはいいからコードはよ、という方は、 次のリンク先からドキュメントと合わせてどうぞ。

背景

現状、下記のようなシェルスクリプトを /etc/ssh/searchkey.sh として用意し、

#!/bin/sh -e

uri="ldap://ldap.example.org"
search_base="ou=People,dc=example,dc=org"
uid=$1
search_filter="(&(objectClass=posixAccount)(uid=${uid})(description=limited))"

ldapsearch -x -LLL -H $uri -b $search_base $search_filter sshPublicKey |\
grep -v '^dn:' | sed '
s/sshPublicKey: //g
s/^ //g
' | tr -d '\n'

このスクリプトのパスを sshd_configAuthorizedKeysCommand に設定することで、 openssh-lpk.schema を使ったLDAPでの公開鍵認証を行っています。ちなみにその前(Ubuntu PreciseやDebian Wheezy)では OpenSSHのソースパッケージにopenssh-lpkパッチを適用して、カスタムビルドパッケージを配布していました。 [LPK0] [LPK1] [LPK2]

問題発生

Trusty, Jessieから AuthorizedKeysCommand でのシェルスクリプトに切り替えてから、しばらく問題がなかったのですが、あるタイミングから公開鍵認証ができなくなりました。 1

原因は2つ。一つは sshPublicKey の値が base64 encodeされている場合。上記のスクリプトだとdecodeしていないため、authorized keysとして機能しません。 もうひとつは、複数の公開鍵が登録されている場合。複数行あった場合の処理も行っておらず、複数のエントリがあった場合は一行に連結されてしまいます。

対応検討

最初、シェルスクリプトで対応しようかと思ったのですが、厄介なのが、複数キーを登録しているユーザで、ある鍵は base64 encodeされておらず、ある鍵はencodeされている、というケースが存在すること。シェルスクリプトだとテストコードも書きづらかったこともあり、ピュアGoで書かれている go-ldapGosh で試してみたら、base64 encodedの値も自動的にdecodeしてくれるので、Golangで書き換えることにしました。

openssh-ldap-pubkey での実装

go-ldapでエラー処理など省いて書くと実質的には下記のようになります。

c, _ := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "<host>", "<port>"))
defer c.Close()
bindRequest := ldap.NewSimpleBindRequest("", "", nil)
c.SimpleBind(bindRequest)
searchRequest := ldap.NewSearchRequest(
        l.base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
                0, 0, false,
                fmt.Sprintf("<filter>", "<uid>"), []string{sshPublicKeyName}, nil)
sr, _ := c.Search(searchRequest)
for _, pubkey := range sr.Entries[0].GetAttributeValues("sshPublicKey") {
             fmt.Println(pubkey)
}

ただ、Goで書き換えても、 openssh-ldap-pubkey コマンドのラッパースクリプトを用意して、LDAPサーバや search baseを指定するのはひと手間増えます。これは避けたいなと思いました。通常LDAPを使っている環境では nslcd も併用していることが多いので、初期リリースとしては /etc/nslcd.conf があればそこから設定を読み込むようにしました。 nslcd.confとopenssh-ldap-pubkeyコマンドのオプションの対応表はドキュメントに記載 してあります。なお、もしnslcd を利用していない場合はラッパースクリプトを用意する必要があります。この場合も ドキュメントに記載 しておきました。

RHEL系は?

皆大好きRHELベースのディストロでは、そもそも openssh-ldap というパッケージが用意されているので、 AuthorizedKeysCommand を使う必要がありません。設定を合わせたい、という場合には同じやり方でできます。

SSSDの対応は?

DebianシステムでもSSSDパッケージは用意されています。ただ、デフォルトではSSSDになっておらず、個人的にもまだ使っていないので、こちらは未対応です。要望があれば対応するかも or パッチウェルカムです。

IPv6 only host in Dual stack でハマった

今の環境は、IPv4/IPv6の dual stackの構成で、IPv6がデフォルトで、ホストはIPv6 onlyまたはIPv4/IPv6のdual stackを選べるようになっています。なのでDNSは基本IPv6のアドレスに問い合わせする構成にしています。LDAPサーバはIPv4しか使えないネットワーク機器も利用するので、例えばldap.example.orgはAAAAレコードとAレコードを設定しています。通常、ldapsearchを行うと基本IPv6経由で行ってくれるので問題ありませんが、今回使ったgo-ldapはdual stackの場合、IPv4で名前解決をしてしまい、アクセスできないとそこで処理を中断してしまうことが分かりました。go-ldapの方を修正して、パッチを投げるのが筋でしょうが、とりあえず、今回はopenssh-ldap-pubkey側で net.LookupHost を使って、IPv6 onlyのホストでも正常に接続できるように 回避策 をとりました。

まとめ

やっつけで作るときはシェルスクリプトやAWK, Sedスクリプトは楽なのですが、メンテナンスを考えると結構しんどいので、テストコード書けるプログラミング言語で実装するほうがやはり良いですね。 2年位前だったら、今回のような場合、Pythonで実装していたと思いますが、クライアントに配布するようなユーティリティで、ディストリビューションの公式パッケージにしていない場合、Golangはシングルバイナリで配布できるのでやはり圧倒的に便利です。

あと、今回のユーティリティは主にDebianシステムが対象なのと、go-ldapは 公式Debianパッケージになって いるのでDebianパッケージ化しておくと、Ubuntu 16.04やDebian Stretch以降で便利です。気が向いたら ITPするかもしれません。

参考文献

LPK0

OpenSSH LDAP public key

LPK1

Applying openssh-lpk to Wheezy

LPK2

How to build custom Debian package automatically by Jenkins

footnotes

1

と言ってもカスタムビルドパッケージを使ったPreciseやWheezyでは問題が無く、現環境ではCentOSに比べ、Ubuntuを使っている人そもそも少ないこと、後述のbase64 encodeされているのも一部のユーザであるため、気づいている人が少ないのが現状でした。

Upgraded to OS X El Capitan

会社用のMacBook Pro(Retina, 15-inch, Early 2013)と私物のMacBook Pro 8,2(15-inch)をEl Capitanにアップグレードしてみました。 一部、騒がれているような問題にも特に遭遇することもなく、2台とも正常にアップグレードできました。

メイン環境のVirtualBox上のSidさんは今日も日常通りです。VirtualBoxをフルスクリーンでしか基本使ってないのですけどね。

VirtualBox入れてない、Mac miniは問題が落ち着いた頃にアップグレードしてみます。

そろそろ私物のMacBook Pro (15-inch)、新しいの欲しいなぁ。

Mac miniといえば

先日、ようやく妻の古いWindowsからiTunesのデータをMac miniに移行しました。まだiTunesに登録してないCDが結構あるので、USB接続のDVDドライブ見てみたら、(使用頻度を考えると)結構高いのですね。うーん。

I have user registration to HP Helion Public Cloud in order to use the Swift

自分で作っているバックアップ用のツール(backup2swift)とそのライブラリ(swiftsc)のIdentity API v3対応のため、KeyStoneとSwiftの環境を作るのが面倒だったのでHP Helion Public Cloudに登録してみました、という使ってみた系のお話です。

結論からいうと、 OpenStack Days Tokyo 2015 で個人情報と引き換えにもらった

OpenStackクラウドインテグレーション オープンソースクラウドによるサービス構築入門

この本の最初の方で紹介されているHP Helion Public Cloudの登録方法を見れば分かるはずだと思います。 1

手間取った点

ユーザーアカウント登録時

一つは、ユーザーアカウント登録の時。まず、contact infoの登録で住所の部屋番号を間違えて登録した記憶がありました。 その後支払い情報を登録するときにも、住所を登録する必要があります。この時には正しく入力しました。 その登録直後に表示されたメッセージには、セキュリティ上の問題があるやらなんやらで確認するのにしばらくかかるよ、みたいな内容が表示されました。なので住所間違えたのがまずかったかな?と思ったわけですが、しばらく(10分くらい?)したらaccount activatedのメールが来たので、メール来るまでの間、なんか面倒なことになってないだろうか嫌だなぁ、ともやもやしていただけで、結果的には杞憂に過ぎませんでした。 2

Identity API用の情報確認

Identity API v2.0 とv3とで、エンドポイント以外に、認証に必要な情報が異なるため、Helionのダッシュボードから探すのに手間取りました。 前者ではユーザー名はユーザーアカウント登録時に設定した User Name 、パスワード、および自動生成される Project Name 、後者では User Nameと対で自動生成される User ID と自分で設置したパスワード、および やはり自動生成される Project ID となんか微妙に異なります。ぱっとみ Project NameProject ID-Project というSuffixを付与すればええんだな、と思ってよく見たら実は違うし。

そういえば、Horizonの画面触ったのはEssex以来で、今同じ部門で進んでいるOpenStackの導入にはまーったく絡んでいないですし 3 。 あとIdentity API v3を触ってたのも v3 がリリースされる前の時だったしなぁ、という言い訳をしておきます。

ConoHaは?

Helion登録する前に ConoHa by GMO でもよいかなと思ったのですが、こちらは まだ Identity API v2.0のみでv3には対応していません でした。残念。

気になっている点

アカウント削除後、再登録できるのか?

Helion は3ヶ月は無料となっているのですが、既に 対応済んだ ので、ほったらかして忘れてしまい課金されてた!というのを避けるために、もう解約しようと思っているのですが、また何らかの理由で必要になったときに、再度新規登録すればまた使えるのかなぁ、というところですね。無料試用期間も復活するとさらに良いのですが。

そういえば日本語情報無いですね

HP Helion関係の日本人エンジニアの方、いると思っているのですが日本語ブログとか無いなぁ、と気づいたのですが、HP Helion Pubilc Cloudを売るのが仕事ではないからなんでしょうかね、と勝手にトスを上げてみて、終わります。

footnote

1

職場で同僚に貸してしまっていて、手元に無いので実際に登録するときにはこの本見てません。

2

実際にこの後contact infoを確認したらやっぱり住所間違えていました。

3

私はというと、5月から引き継いだシステムのいわゆる「技術的負債の返済」を先週までやっていました。