LDAPでsudoを一元管理する

=====================

## 環境

- CentOS 6.5
- OpenLDAP

## Overview

まともに管理すると煩雑になりがちなsudoをLDAPで一元的に管理したい。

## Installation

### with Ansible

インストール手順は全てplaybook化してあります。

https://github.com/shufo/ansible-sudo-ldap

```
$ git clone git@github.com:shufo/ansible-sudo-ldap.git
$ cd ansible-sudo-ldap
$ vagrant up
```

でVagrant仮想マシン上に以下の手動でのインストール手順を再現したOpenLDAP+sudoなテスト環境が出来ます。(要Ansible, Vagrant)

立ち上がったら

```
$ vagrant ssh
[vagrant@vagrant ~]$ sudo su
[root@vagrant ~]$ su - test
[test@vagrant ~]$ sudo cat /var/log/secure 
"Enter [test]'s Password:" password
```

で動作確認出来ます。

リモートに構築する場合は`ansible_hosts`ファイルを編集し

```ansible_hosts
default ansible_ssh_host=127.0.0.1 ansible_ssh_port=22
```

playbookを直接実行してください。

```shell
ansible-playbook site.yml -i ansible_hosts
```

クライアント側のみ構築する場合は`tasks/main.yml`のLDAPサーバ側用のタスクをコメントアウトしてください。

```python
---
# - include: server.yml
- include: client.yml
```

### 手動

#### LDAPサーバ設定

- OpenLDAP、依存パッケージのインストール

  ```
  yum install openldap openldap-servers openldap-clients pam_ldap 
  ```

- LDAPディレクトリ初期化

  ```bash
mkdir /var/lib/ldap
chown ldap:ldap /var/lib/ldap
rm -fR /etc/openldap/slapd.d/*
```

- 設定ファイルコピー

  ```bash
cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG
cp /usr/share/openldap-servers/slapd.conf.obsolete /etc/openldap/slapd.conf
```

- LDAPサーバのrootパスワード生成

  ```bash
/usr/sbin/slappasswd -s password
```

```/etc/openldap/slapd.conf
# スキーマファイル設定
include /etc/openldap/schema/corba.schema
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/duaconf.schema
include /etc/openldap/schema/dyngroup.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/java.schema
include /etc/openldap/schema/misc.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/openldap.schema
include /etc/openldap/schema/ppolicy.schema
include /etc/openldap/schema/collective.schema

# 接続プロトコル
allow bind_v2

# 管理ファイル
pidfile     /var/run/openldap/slapd.pid
argsfile    /var/run/openldap/slapd.args

# TLS設定
#TLSCACertificatePath  /etc/openldap/ssl/cacert.pem
#TLSCertificateFile    /etc/openldap/ssl/server.crt
#TLSCertificateKeyFile /etc/openldap/ssl/server.key

# userPasswordに関するアクセス権
access to attrs=userPassword
    by self write
    by dn="cn=Directory Manager,dc=example,dc=com" write
    by anonymous auth
    by * none

# その他の属性に対するアクセス権
access to *
    by self write
    by dn="cn=Directory Manager,dc=example,dc=com" write
    by * read

# monitorデータベースに対するアクセス権
database monitor
access to *
    by dn.exact="cn=Directory Manager,dc=example,dc=com" read
    by * none

# データベース設定
database    bdb
suffix      "dc=example,dc=com"
checkpoint  1024 15
rootdn      "dc=example,dc=com"
rootpw      {SSHA}jadajsdna〜中略〜hogehoge
directory   /var/lib/ldap

# indexの設定
index objectClass                       eq,pres
index ou,cn,mail,surname,givenname      eq,pres,sub
index uidNumber,gidNumber,loginShell    eq,pres
index uid,memberUid                     eq,pres,sub
index nisMapName,nisMapEntry            eq,pres,sub

```


- LDAPサーバにsudo用のschemaを読みこませる

  `schema.OpenLDAP`をスキーマに追加。

  ```bash
  cp /usr/share/doc/sudo-1.8.6p3/schema.OpenLDAP /etc/openldap/schema/sudo.schema
  ```
  
  /etc/openldap/slapd.confに

  ```/etc/openldap/slapd.conf
  include /etc/openldap/schema/sudo.schema
  ```

  を追加。

- openldapを起動

  ```
  service slapd start
  ```

- エントリを作成する

  ベースになるエントリを作成します。

  ```add_ou.ldif
  dn: dc=example,dc=com
  objectClass: dcObject
  objectClass: organization
  dc: example
  o: example

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

  dn: ou=groups,dc=example,dc=com
  objectClass: organizationalUnit
  ou: groups
  ```

  ```bash
  ldapadd -D "cn=Directory Manager,dc=example,dc=com" -w password -f add_ou.ldif
  ```

- ユーザを作成する

  サンプルとして`test`ユーザを作成する。

  ```add_user.ldif
  dn: uid=test,ou=people,dc=example,dc=com
objectClass: account
objectClass: posixAccount
uid: test
cn: test
userPassword: {SSHA}hogehoge〜中略〜
loginShell: /bin/bash
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/test
  ```
  
  `posixAccount`クラスの必須属性は`loginShell`、`uidNumber`、`gidNumber`、`homeDirectory`なのでこの４つは最低限含める必要があります。
  
  ```bash
  ldapadd -D "cn=Directory Manager,dc=example,dc=com" -w password -f add_user.ldif
  ```

- グループを作成する

  サンプルとして`members`グループを作成する。

  ```add_group.ldif
  dn: cn=members,ou=groups,dc=example,dc=com
  objectClass: top
  cn: members
  objectClass: posixGroup
  gidNumber: 1000
  ```

  ```bash
  ldapadd -D "cn=Directory Manager,dc=example,dc=com" -w password -f add_group.ldif
  ```

- sudoers用ouを作成する


  ```
  vim sudoersOU.ldif
  ```

  ```sudoersOU.ldif
  dn: ou=SUDOers,dc=example,dc=com
  description: SUDOers
  objectClass: organizationalUnit
  objectClass: top
  ou: SUDOers
  ```

  ```bash
  ldapadd -x -D "cn=Directory Manager,dc=example,dc=com" -w password -f sudoersOU.ldif
  ```

- /etc/sudoersを編集する

  ```bash
  [root@agenttest user.0]# cat /etc/sudoers | grep -v "^#" | grep -v "^\s*$"
Cmnd_Alias FILE_READ = /bin/cat, /bin/more, /usr/bin/tail
Cmnd_Alias SERVICE = /sbin/service
Cmnd_Alias NETWORK_LOOKUP = /bin/ping, /bin/netstat, /usr/bin/nslookup
Cmnd_Alias DELEGATING = /bin/chown, /bin/chmod, /bin/chgrp
Defaults   !visiblepw
Defaults    always_set_home
Defaults    env_reset
Defaults    env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS"
Defaults    env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE"
Defaults    env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES"
Defaults    env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE"
Defaults    env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY"
Defaults    !root_sudo
Defaults    !lecture
Defaults    log_host
Defaults    log_year
Defaults    logfile=/var/log/sudo.log
Defaults    ignore_dot
Defaults    ignore_local_sudoers
Defaults    timestamp_timeout=0
Defaults    passprompt="Enter [%u]'s Password:"
Defaults    badpass_message="The password is not corresponding. Try again."
Defaults    insults
Defaults    secure_path=/sbin:/bin:/usr/sbin:/usr/bin
root	ALL=(ALL) 	ALL
%members       ALL=(ALL) FILE_READ
%wheel ALL=NOPASSWD: ALL
  ```

解説: `Cmnd_Alias`で定義したコマンドの集まりを各グループに対して割り当てている。
`%members       ALL=(ALL) FILE_READ`で`members`グループに`FILE_READ`で定義した`/bin/cat, /bin/more, /usr/bin/tail`コマンドの実行を許可している。`members`グループのユーザに対して`ALL`(全て)のホストから`ALL`(任意)のユーザ権限で`FILE_READ`で定義したコマンドが実行可能という意味。`less`ではなく`more`を許可しているのは`less`は`:!/bin/bash`とless内でコマンドを実行することでrootのシェルを取得出来るから。
`!root_sudo`でrootでのsudoを禁止、`!lecture`で最初にでる説明を非表示、`ignore_local_sudoers`でLDAP参照のみにしてローカルの/etc/sudoersを無視するようにしている。


- `sudoers2ldif`でldifファイルに変換する

  ```bash
  export SUDOERS_BASE=ou=SUDOers,dc=example,dc=com
  cat /etc/sudoers | perl /usr/share/doc/sudo-1.8.6p3/sudoers2ldif > sudoers.ldif
  ```

  ```sudoers.ldif
  dn: cn=defaults,ou=SUDOers,dc=example,dc=com
  objectClass: top
  objectClass: sudoRole
  cn: defaults
  description: Default sudoOption's go here
  sudoOption: !visiblepw
    sudoOption: always_set_home
  sudoOption: env_reset
  sudoOption: env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS"
  sudoOption: env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE"
  sudoOption: env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES"
  sudoOption: env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE"
  sudoOption: env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY"
  sudoOption: !root_sudo
  sudoOption: !lecture
  sudoOption: log_host
  sudoOption: log_year
  sudoOption: logfile=/var/log/sudo.log
  sudoOption: ignore_dot
  sudoOption: ignore_local_sudoers
  sudoOption: timestamp_timeout=0
  sudoOption: passprompt="Enter [%u]'s Password:"
  sudoOption: badpass_message="The password is not corresponding. Try again."
  sudoOption: secure_path=/sbin:/bin:/usr/sbin:/usr/bin
    
  〜中略〜
  ```



- LDIFをインポートする

  `sudoers2ldif`で変換したLDIFをインポートする

  ```bash
  ldapadd -x -D "cn=Directory Manager,dc=example,dc=com" -w password -f sudoers.ldif
  ```

#### LDAPクライアント側設定

  - 依存パッケージのインストール

  ```bash
  yum install openldap-clients nss-pam-ldapd pam_ldap openssh-ldap authconfig
  ```

  - 認証周りを`authconfig`で設定

  ```bash
  # LDAPによるユーザ情報参照を有効にする
  authconfig --enableldap --update
  # LDAPによる認証を有効にする
  authconfig --enableldapauth --update
  # ユーザのホームディレクトリの自動作成を有効にする
  authconfig --enablemkhomedir --update
  # shadowパスワードの使用を有効にする
  authconfig --enableshadow --update  
  # local認証を有効にする
  authconfig --enablelocauthorize --update
  # LDAPサーバのURIを指定
  authconfig --ldapserver=ldap://127.0.0.1:389 --update
  # LDAPサーバのベースDNを指定
  authconfig --ldapbasedn=dc=example,dc=com --update 
  ```  

  以上のコマンドを実行した結果のPAMの設定は以下です。

  ```/etc/pam.d/system-auth
  #%PAM-1.0
  # This file is auto-generated.
  # User changes will be destroyed the next time authconfig is run.
  auth        required      pam_env.so
  auth        sufficient    pam_unix.so nullok try_first_pass
  auth        requisite     pam_succeed_if.so uid >= 500 quiet
  auth        sufficient    pam_ldap.so use_first_pass
  auth        required      pam_deny.so
  
  account     required      pam_unix.so broken_shadow
  account     sufficient    pam_localuser.so
  account     sufficient    pam_succeed_if.so uid < 500 quiet
  account     [default=bad success=ok user_unknown=ignore] pam_ldap.so
  account     required      pam_permit.so
  
  password    requisite     pam_cracklib.so try_first_pass retry=3 type=
  password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
  password    sufficient    pam_ldap.so use_authtok
  password    required      pam_deny.so
  
  session     optional      pam_keyinit.so revoke
  session     required      pam_limits.so
  session     optional      pam_mkhomedir.so skel=/etc/skel umask=077
  session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
  session     required      pam_unix.so
  session     optional      pam_ldap.so
  ```

  `pam_mkhomedir`モジュールを有効にしているのでユーザのホームディレクトリが存在しない場合自動的に作られます。自動削除はされないので必要なくなったら削除しましょう。

  ```/etc/pam.d/password-auth
  #%PAM-1.0
  # This file is auto-generated.
  # User changes will be destroyed the next time authconfig is run.
  auth        required      pam_env.so
  auth        sufficient    pam_unix.so nullok try_first_pass
  auth        requisite     pam_succeed_if.so uid >= 500 quiet
  auth        sufficient    pam_ldap.so use_first_pass
  auth        required      pam_deny.so
  
  account     required      pam_unix.so broken_shadow
  account     sufficient    pam_localuser.so
  account     sufficient    pam_succeed_if.so uid < 500 quiet
  account     [default=bad success=ok user_unknown=ignore] pam_ldap.so
  account     required      pam_permit.so
  
  password    requisite     pam_cracklib.so try_first_pass retry=3 type=
  password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
  password    sufficient    pam_ldap.so use_authtok
  password    required      pam_deny.so
  
  session     optional      pam_keyinit.so revoke
  session     required      pam_limits.so
  session     optional      pam_mkhomedir.so skel=/etc/skel umask=077
  session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
  session     required      pam_unix.so
  session     optional      pam_ldap.so
  ```
  
  - sudo-ldapの設定

  `sudo-ldap.conf`は`authconfig`では設定出来ないので手動で設定します。


  ```bash
  [root@localhost]# sudo -V
  ldap.conf path: /etc/sudo-ldap.conf

  [root@localhost ]# cat /etc/sudo-ldap.conf  | grep -v "^#" | grep -v "^\s*$"
  binddn cn=Directory Manager
  bindpw password
  uri ldap://127.0.0.1:389
  sudoers_base ou=SUDOers,dc=example,dc=com
  sudoers_debug 1
  ```

  - sudoersの参照先をldapに切り替え
  
  ```
  [root@localhost]# cat /etc/nsswitch.conf | grep sudoers
  sudoers:	ldap files
  ```

  - 確認

  他ユーザにsuして確認してみる。
 
  ```bash
  [root@localhost]# su - test
  [shufo@localhost ~]$ sudo cat /var/log/secure 
  sudo: ldap_set_option: debug -> 0
  sudo: ldap_set_option: ldap_version -> 3
  sudo: ldap_sasl_bind_s() ok
  sudo: Looking for cn=defaults: cn=defaults
  sudo: found:cn=defaults,ou=SUDOers,dc=example,dc=com
  sudo: ldap search '(|(sudoUser=test)(sudoUser=%test)(sudoUser=%#501)(sudoUser=ALL))'
  sudo: searching from base 'ou=SUDOers,dc=example,dc=com'
  sudo: adding search result
  sudo: result now has 0 entries
  sudo: ldap search '(sudoUser=+*)'
  sudo: searching from base 'ou=SUDOers,dc=example,dc=com'
  sudo: adding search result
  sudo: result now has 0 entries
  sudo: sorting remaining 0 entries
  sudo: searching LDAP for sudoers entries
  sudo: done with LDAP searches
  sudo: user_matches=1
  sudo: host_matches=0
  sudo: sudo_ldap_lookup(0)=0x40
  Enter [test]'s Password: [パスワード入力]
  test is not allowed to run sudo on localhost.  This incident will be reported.
  ```

  LDAPサーバに問い合わせされているのがログから分かる。
  実際に問い合わせているクエリはbaseDNの`ou=SUDOers,dc=example,dc=com`を`(|(sudoUser=shufo)(sudoUser=%shufo)(sudoUser=%#501)(sudoUser=ALL))`で検索している。