suするuserを限定したい

# はじめに

個人的には「サーバのアカウント管理はシンプルにすべき」で、SSHログイン可能なユーザは

* 管理ユーザ（wheelあり）
* 構成管理用ユーザ（wheelなし、sudo設定あり）

の2ユーザを共通で用意しておけば十分だと考えています。[^1]
ミドルウェアが利用するユーザのように増減することが滅多にないユーザは構成管理ツールを使えば管理コストはあまりかかりませんが、人間がログインして利用するユーザ（＝増減する可能性が高いユーザ）をサーバ毎に管理するのは運用が辛くなります。

ただ、最近は1つのサーバに対して複数の組織体が関与して運用するプロジェクトに関わっており、管理ユーザを共用するのは主に監査の面でよろしくないため、組織毎に管理ユーザを作成しています。
このとき組織毎の管理ユーザは「wheelあり」とするか「wheelなし、sudo設定あり」のどちらかにするかと思いますが、前述したプロジェクトでは後者の方が一般的な設定かと思います。[^2]
前者の場合、rootパスワードを知っている人がrootにスイッチ可能な状態ですが、特権ユーザを利用可能な組織が複数に渡るのは事故の元であり、「利用可能な人＝パスワードを知っている人」という曖昧なグループは管理不可能だからです。

ところが「運用上の都合」で`su`を組織毎の管理ユーザについても使えるようにしなければならないケースが出てきました。
単純にwheelグループに追加すると前述の問題があるので、

* `su`を使うことができるユーザ
* スイッチ先のユーザ

のそれぞれを管理できるようにしたいと考えていたところ、幸いにして昔からそういう要件はあったようで、今回は[Permitting or Restricting a User's `su` Access to Privileged Accounts](https://access.redhat.com/solutions/64860)を参考にして

* 組織A用ユーザ（wheelなし、ミドルウェアユーザへのスイッチ可）

というユーザを作成していきます。


# PAMの動作概要（一部）

検証する前にPAMの設定ファイルの書式や動作概要を理解しておきます。
参考にしたドキュメントを以下に挙げておきます。

* [PAMの設定ファイル概要](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system-level_authentication_guide/pam_configuration_files)
* [PAMで利用可能なModule一覧](http://www.linux-pam.org/Linux-PAM-html/sag-module-reference.html)
* `man pam.d`

今回は認証周りなのでmodule interfaceは`auth`だけに絞って整理していきます。


## PAM の設定ファイル

PAMの設定ファイルの書式は以下の通りです。

```
module_interface  control_flag  module_name  module_arguments
```

このうちControl Flagはmoduleの戻り値に応じた処理を定義します。
Control Flagの書式は

```
[value1=action1 value2=action2 ...]
```

となっており、"moduleの戻り値が`value`の場合は`action`を実行する"という意味になります。
原則各行の処理結果をスタックしていき、それらが総合的に判断されてPAMの処理結果になります。
`value`はPAMモジュールの戻り値の定義が多いので割愛しますが、`action`として利用できるものは以下の通りです。

|Action|処理|
|:--|:--|
|ignore|この行を無視する|
|bad   |この行を失敗とする|
|die   |badと同じだが、即時PAMの処理を終了する|
|ok    |この行を成功とする（直前の処理結果が失敗でない限り、この行の戻り値で上書きする）|
|done  |okと同じだが、即時PAMの処理を終了する|
|N     |この行を成功とし、次のN個のmoduleをスキップする（N>0としなければならない）|
|reset |これまでの処理結果のスタックを初期化する|

一般的にPAMのポリシーで使われている`required`などは利用頻度の高い処理を省略した表記になっています。

|表記|本来の表記|処理|
|:--|:--|:--|
|required|[success=ok new_authtok_reqd=ok ignore=ignore default=bad]|moduleの処理が成功しなかった場合、失敗とする|
|requisite|[success=ok new_authtok_reqd=ok ignore=ignore default=die]|moduleの処理が成功しなかった場合、失敗として即終了する|
|sufficient|[success=done new_authtok_reqd=done default=ignore]|moduleの処理が成功した場合、成功として即終了する|
|optional|[success=ok new_authtok_reqd=ok default=ignore]|moduleの処理結果は無視する|

また、他のファイルの内容を読み込む用のフラグとして`include`と`substack`があります。

|表記|意味|
|:--|:--|
|`include` |引数に指定されたファイルを読み込む（ファイルの内容をそのまま展開するようなイメージ）|
|`substack`|引数に指定されたファイルを読み込み、その処理結果をこの行の処理結果とする|

上記の情報をもとにいくつかPAMの定義を見ていきます。

## 例1. system-auth

最終行で常にNGになるので、PAMの処理がOKになるのは3行目が成功したときだけになります。

```
auth        required      pam_env.so
auth        required      pam_faildelay.so delay=2000000
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so
```

1. 環境変数をセットできなかったらNG
2. 失敗時の遅延をセットできなかったらNG
3. パスワード認証できたら即時OK
  * パスワードなし（nullok）
  * ユーザから取得済みのパスワードを利用し、取得していない場合は入力を促す（try_first_pass)
4. UIDが1000未満の場合は即時NG
  * 成功ログは記録しない（quiet_success）
5. 常にNG


## 例2. su

rootユーザ以外でPAMの処理がOKになるためは「wheelグループに所属していること」と「パスワード認証に通ること(=`system-auth`の結果がOK)」の2つが必要となります。

```
auth		sufficient	pam_rootok.so
auth		required	pam_wheel.so use_uid
auth		substack	system-auth
auth		include		postlogin
```

1. rootユーザの場合はOK
2. 現在のUIDをもとに判定し、wheelグループに所属していない場合はNG
3. `system-auth`を読み込み、その結果をこの行の判定結果とする
4. `postlogin`を読み込む


## 例3. Knowledgebaseの設定

skipを使ってgroupaに所属していない場合は影響がでないポリシーになっています。

```
auth           [success=2 default=ignore] pam_succeed_if.so use_uid user notingroup groupa
auth           required pam_wheel.so use_uid group=groupa
auth           required pam_listfile.so item=user sense=allow onerr=fail file=/etc/security/su-groupa-access
```

1. 現在のUIDをもとに判定し、groupaに所属していない場合は次の2つのモジュールをスキップする
2. 現在のUIDをもとに判定し、groupaに所属していない場合はNG
3. スイッチ先のユーザ名が`/etc/security/su-groupa-access`に含まれていない場合はNG


# 実際にやってみる

* OS: CentOS 7.6 (vagrant box)

## 事前準備

検証用のユーザの作成は以下の通り。

* admin(管理者: wheelあり)
* auser01(groupaユーザ, ansibleにのみスイッチ可能)
* buser01(groupbユーザ, スイッチ禁止)
* ansible(ミドルウェアユーザ)

```shell-session:グループ/ユーザ作成
# groupadd -g 1001 groupa
# groupadd -g 1002 groupb
# useradd -g users -G wheel admin
# useradd -g groupa auser01
# useradd -g groupb buser01
# useradd ansible
# echo 'admin:********' | chpasswd
# echo 'auser01:********' | chpasswd
# echo 'buser01:********' | chpasswd
# echo 'ansible:********' | chpasswd
```

```shell-session:確認
# id admin
uid=2004(admin) gid=100(users) groups=100(users),10(wheel)
# id auser01
uid=2005(auser01) gid=1001(groupa) groups=1001(groupa)
# id buser01
uid=2006(buser01) gid=1002(groupb) groups=1002(groupb)
# id ansible
uid=2007(ansible) gid=2007(ansible) groups=2007(ansible)
```

groupaに所属するユーザがスイッチ可能なユーザ一覧を記載するファイルを作成。

```text:/etc/security/su-groupa-access
ansible
```

PAMの設定は以下の通り。

```diff:/etc/pam.d/su
 #%PAM-1.0
 auth		sufficient	pam_rootok.so
+auth		[success=1 ignore=1 default=ignore]	pam_wheel.so use_uid group=groupa
+auth		required	pam_wheel.so use_uid
+auth		[success=1 default=ignore]	pam_succeed_if.so use_uid user notingroup groupa
+auth		required	pam_listfile.so item=user sense=allow onerr=fail file=/etc/security/su-groupa-access
 auth		substack	system-auth
 auth		include		postlogin
```

2-3行目がsuできるユーザの判定、4-5行目がgroupaに対するスイッチ先のユーザの判定になっています。
pam_wheel.soが若干曲者でwheelグループに所属していると判定された場合、原則`PAM_IGNORE`を返す（`debug`オプションをつけると分かる）ので、ignoreに対してもskipを記載する必要があリます。

なおvagrant box利用時にrootまたはvagrant以外のユーザからrootユーザへのスイッチが`account`で許可されていない場合があるので注意して下さい。修正する箇所は以下の通り。

```diff:/etc/pam.d/su
 account		sufficient	pam_succeed_if.so uid = 0 use_uid quiet
 account		[success=1 default=ignore] pam_succeed_if.so user = vagrant use_uid quiet
-account		required	pam_succeed_if.so user notin root:vagrant
+account		required	pam_succeed_if.so user notin vagrant
 account		include		system-auth
```


## 確認

adminはrootもansibleもスイッチOK。

```shell-session:admin
[admin@localhost ~]$ su -
Password:
Last login: 日  4月  7 07:22:43 UTC 2019 on pts/1
[root@localhost ~]# logout
[admin@localhost ~]$ su - ansible
Password:
Last login: 日  4月  7 07:22:31 UTC 2019 on pts/1
[ansible@localhost ~]$ logout
```

auser01はrootはスイッチNG, ansibleはスイッチOK。

```shell-session:auser01
[auser01@localhost ~]$ su -
Password:
su: Authentication failure
[auser01@localhost ~]$ su - ansible
Password:
Last login: 日  4月  7 07:23:54 UTC 2019 on pts/1
[ansible@localhost ~]$ logout
[auser01@localhost ~]$
```

buser01はroot, ansibleともにスイッチNG。

```shell-session:buser01
[buser01@localhost ~]$ su -
Password:
su: Authentication failure
[buser01@localhost ~]$ su - ansible
Password:
su: Authentication failure
[buser01@localhost ~]$
```

ちなみにpam_wheel.soで弾かれた場合でもパスワードを間違えた場合でも、エラーメッセージは「Authentication failure」が出ますが、どちらで拒否されたか確認したいときは/var/log/secureを確認してください。

```text:/var/log/secure（パスワードを間違えた場合）
Apr  7 07:49:13 localhost su: pam_unix(su-l:auth): authentication failure; logname=auser01 uid=2005 euid=0 tty=pts/1 ruser=auser01 rhost=  user=ansible
```


[^1]: 各サーバの前段にJump Box（踏み台）があり、そのJump Boxで個人ユーザが識別、認証できていることが前提です。
[^2]: 複数の組織体が関わる場合、作業範囲が定められており、必要な権限も決まっているので`sudo`によるコマンドレベルの制御で大体事足りるはず。