続：suするuserを限定したい

[前回](https://qiita.com/ochiba/items/5729f76a916104edf246 "前回")の続きのようなもの。

# はじめに

新しい要件を抜粋すると以下の通りです。

- sshログイン可能なユーザを一部に限定したい
- シェルログインが必要なユーザはsshログインできるユーザからsuする
- ログイン可能なユーザへの直接のsuは不可とする
- rootにsuできるユーザは限定する
- **suする際はパスワード認証を不要とする**

これを受けて自分が設計したのが以下のユーザ構成です。

![PAM-su_grouping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232458/fb3e5308-85ae-e153-4182-86fc8768944d.png)

個人的な意見（主にセキュリティ面で）は差し控えますが、最後の要件が曲者でパッと思いつくのはsudoの`NOPASSWD`ラベルを使うことでした。
しかしながらrootにsuできるユーザを限定する要件でホワイトリストにせざるを得ず、su先のユーザが増えると地獄なので不採用としました。
結局、~~みんなが大好きな~~PAMで制御する方針で実装。

# 検証
## 事前準備

検証用のユーザを作成します（前述の図に出てきたグループはPrimaryGroupとしては使わないのがポイント）。

```console
# for usr in admin opusr appX appY appZ ; do useradd -g users $usr && echo "$usr:secret" | chpasswd && id $usr ; done
uid=1001(admin) gid=100(users) groups=100(users)
uid=1002(opusr) gid=100(users) groups=100(users)
uid=1003(appX) gid=100(users) groups=100(users)
uid=1004(appY) gid=100(users) groups=100(users)
uid=1005(appZ) gid=100(users) groups=100(users)
```

制御用グループを作成します。内訳は以下の通り。

|GroupName|ssh|su(root)|su(admins)|su(ops)|su(works)|Desc|
|:-:|:-:|:-:|:-:|:-:|:-:|:--|
|rusers|○|×|×|×|×|sshログイン許可グループ|
|admins|×|○|-|×|○|su許可グループ①|
|ops   |×|×|×|-|○|su許可グループ②|
|works |×|×|×|×|-|su対象グループ|

```console
# for grp in rusers admins ops works ; do groupadd $grp && grep "^$grp" /etc/group ; done
rusers:x:1001:
admins:x:1002:
ops:x:1003:
works:x:1004:
```

## sshログインできるユーザの制御

sshd_configで`AllowGroups`を設定します。

```diff:/etc/ssh/sshd_config
+AllowGroups	rusers
```

```console
# sshd -t && echo OK
OK
# systemctl reload sshd.service
# systemctl status sshd.service
● sshd.service - OpenSSH server daemon
   Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor preset: enabled)
   Active: active (running) since ...
```

sshログインを許可したいユーザに`rusers`を付与します。

```console
# for usr in admin opusr ; do usermod -aG rusers $usr && id $usr ; done
uid=1001(admin) gid=100(users) groups=100(users),1001(rusers)
uid=1002(opusr) gid=100(users) groups=100(users),1001(rusers)
```

クライアントから接続テスト。

```console:rusersに所属している場合
❯ ssh 192.168.1.30 -l admin
Warning: Permanently added '192.168.1.30' (ECDSA) to the list of known hosts.
admin@192.168.1.30's password:
[admin@clt00 ~]$
```

```console:rusersに所属していない場合
❯ ssh 192.168.1.30 -l appX
Warning: Permanently added '192.168.1.30' (ECDSA) to the list of known hosts.
appX@192.168.1.30's password:
Permission denied, please try again.
```

`AllowGroups`によって制限された場合は以下のログが出ます。

```text:/var/log/secure
Sep 17 10:37:56 clt00 sshd[701]: User appX from 192.168.1.1 not allowed because none of user's groups are listed in AllowGroups
```

## suできるユーザの制御

PAMの設定ファイルを更新します。デフォルトの設定との差分は以下の通り。

```diff:/etc/pam.d/su
 #%PAM-1.0
 auth		sufficient	pam_rootok.so
 # Uncomment the following line to implicitly trust users in the "wheel" group.
 #auth		sufficient	pam_wheel.so trust use_uid
 # Uncomment the following line to require a user to be in the "wheel" group.
 #auth		required	pam_wheel.so use_uid
+auth		sufficient	pam_succeed_if.so use_uid user ingroup admins
+auth		sufficient	pam_succeed_if.so use_uid user ingroup ops
+auth		required	pam_deny.so
 auth		substack	system-auth
 auth		include		postlogin
+
 account	sufficient	pam_succeed_if.so uid = 0 use_uid quiet
+account	[success=ignore default=1] \
+				pam_succeed_if.so use_uid user ingroup admins
+account	sufficient	pam_succeed_if.so user in root
+account	sufficient	pam_succeed_if.so user ingroup works
+account	required	pam_deny.so
 account	include		system-auth
+
 password	include		system-auth
+
 session	include		system-auth
 session	include		postlogin
 session	optional	pam_xauth.so
```

追加したエントリをざっくり説明すると以下の通りです。

- `auth`でsuできるユーザを制御する
 - adminsグループのユーザはsuを許可する
 - opsグループのユーザはsuを許可する
- `account`でsu先のユーザを制御する
 - adminsグループのユーザはsu先がrootユーザの場合、即時OKとする
 - su先がworksグループのユーザの場合、即時OKとする
 - system-authの`account`はユーザの正統性が確認されるとOKを返してしまうので、その前にNGを入れておく（system-authのincludeは正直消しても良いです）

エントリを書く際に注意したいのが、`pam_succeed_if`モジュールの`use_uid`の有無です。モジュールの説明がちょっとわかりづらいんですが、suで利用した場合は以下の挙動となります。

- `use_uid`をセットする → suで切り替える**前**のユーザで判定
- `use_uid`をセットしない → suで切り替えた**後**のユーザで判定

これでグループに対する制御設定が完了したので、あとはグループの定義に応じてSubGroupに追加していくだけです。

```console
# for usr in admin ; do usermod -aG admins $usr ; id $usr ; done
uid=1001(admin) gid=100(users) groups=100(users),1001(rusers),1002(admins)

# for usr in opusr ; do usermod -aG ops $usr ; id $usr ; done
uid=1002(opusr) gid=100(users) groups=100(users),1001(rusers),1003(ops)

# for usr in appX appY appZ ; do usermod -aG works $usr ; id $usr ; done
uid=1003(appX) gid=100(users) groups=100(users),1004(works)
uid=1004(appY) gid=100(users) groups=100(users),1004(works)
uid=1005(appZ) gid=100(users) groups=100(users),1004(works)
```

各ユーザでsuのテスト。

```console:adminユーザの場合
[admin@clt00 ~]$ su -
[root@clt00 ~]# logout
// admin->root: OK

[admin@clt00 ~]$ su - opusr
su: Authentication failure
// admin->opusr: NG

[admin@clt00 ~]$ su - appX
[appX@clt00 ~]$ logout
[admin@clt00 ~]$ su - appY
[appY@clt00 ~]$ logout
[admin@clt00 ~]$ su - appZ
[appZ@clt00 ~]$ logout
// admin->app[XYZ]: OK
```

```console:opusrユーザの場合
[opusr@clt00 ~]$ su -
su: Authentication failure
// opusr->root: NG

[opusr@clt00 ~]$ su - admin
su: Authentication failure
// opusr->admin: NG

[opusr@clt00 ~]$ su - appX
[appX@clt00 ~]$ logout
[opusr@clt00 ~]$ su - appY
[appY@clt00 ~]$ logout
[opusr@clt00 ~]$ su - appZ
[appZ@clt00 ~]$ logout
// opusr->app[XYZ]: OK
```

# 最後に

「グループに権限を持たせて、その付け外しでユーザの権限制御を行う」のはAWSなどのクラウドを始めとして、一般的なやり方になってきたと思いますが、今回はそれを利用してみました。
今回の設定の場合、直接admin→opusrでsuはできませんがrootを経由すればopusrにsuできてしまうことには留意してください（一応防ぐ手立てはありますが、rootユーザにsuできる時点でPAMの設定も更新できてしまうのであまり意味はありません・・・）。
また、suする頻度が多いとログもその分増えるので、ある程度検証ができたら`quiet`オプションを足してログの出力を抑えることも検討した方がよいです。