389DSのACIをCockpitのGUIで設定する

前回まででApacheにKerberos認証を入れた所までやりましたが、認可の設定がまだでした。

https://qiita.com/jp_ibis/items/7adfec397ef099670c28

Apacheに乗っかっているWebアプリケーションで基本的なグループを使った認可をやりたいのですが、Kerberosはご存じの通り認証の技術であって認可はできません。今回の一連の取り組みではKerberos認証＋LDAP認可の仕組みで作っているので、Webアプリケーションでの認可にはLDAPを使用します。

Webアプリごとにグループ情報にアクセスするためのDNやパスワードを設定しているとどこかで必ずミスして漏洩するので、ApacheでDNとパスワードを取得しておき環境変数経由でWebアプリに受け渡します。

まずは現在までの構成をおさらいです。

```mermaid
flowchart TD

minipc["minipc.home<br>(Kerberos/LDAPS)"]--Keytab-->Cockpit--TLS-->Desktop
minipc--SSSD-->Samba-->Desktop
minipc--Keytab-->Apache--TLS-->Desktop
minipc--SSSD-->SSHD-->NotePC
SSHD-->Desktop
minipc--SSSD-->Login-->NotePC
```

SSSD経由のときはKeberos認証と共にLDAPのグループを取得できるように設定されています。

```conf:/etc/sssd/sssd.conf
...
# 認可用
ldap_group_search_base = ou=Groups,dc=home
ldap_id_mapping = false
...
```

ところがKeytabを使うサービスはGSSAPIで直接Keberosとやりとりしちゃうので、LDAPからの情報は自力で取得する必要があります。

# グループ情報へのアクセス用ユーザーを作る

Webアプリケーションからグループ情報にアクセスするためのユーザーを作ります。アクセスするための情報は何らかの形でApacheやその先のWebアプリケーションに取り込むため、基本的には漏洩するものとして考えます。

少なくともWebアプリケーション開発者はDNとパスワードを取得できるので、そこから先も考慮にいれるとほぼ**確実**に漏洩します。

なので**漏洩しても問題無い範囲しかアクセスできないユーザー**でアクセスするという考え方になります。LDAP全体の管理者アカウントでのアクセスはダメ絶対。

それでは管理用ユーザーを格納する`ou=AdminUsers`というグループを作り`cn=group_reader`というユーザーを作りましょう。今回はnsPersonを使いました(cnがキーになりuserPasswordを持っている最小objectClass)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/6414287c-0c54-4aab-bc8e-0ba87e58dfd7.png)

**※漏洩前提でも一応パスワードはしっかり設定しておきましょう**

# ACIを設定する

この記事のメインです。ウイザードの使い方が微妙にわかりにくかったのでメモしておこうと。


まず **アクセスコントロール対象** のグループを選択しACIs設定ウィザードを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/af902e2d-9fc7-4e4c-ab02-8b6615c477b4.png)

ACI追加ウィザードボタンで追加を開始します。コピペ等で手動でACIを追加するボタンで一気に設定することもできますし、後からACI編集ボタンで修正を行うこともできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/f8bf04ac-d969-4747-8497-77d09e2d5329.png)

## ACIの名称とターゲットの設定(target)

ウィザードを実行すると名前と対象の入力を求められます。対象を選択してウィザードを立ち上げているので対象はたぶんそのままのはずです。設定名だけ決めちゃいましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/f840208d-ed12-4135-a45a-854a751b134e.png)

## ターゲット属性(targetattr)

設定すると上手く動きませんでした_(:3」∠)_

空で進めると`(targetattr="*")`が設定されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/e3aac890-bac8-4f9f-bad0-949dc5de35ae.png)

## ターゲットフィルタ(targetfilter)

検索対象となるグループがもつobjectClassを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/58b07b9d-cab3-429d-8ce6-e17932f2b00f.png)

## エントリの移動

なんですかねこれ(´・ω:;.:... 

Gemini先生に解説をお願いしたけどよく分からなかったのでとりあえず空にしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/91339bc3-1b50-45ed-a330-4309bfc74788.png)

## アクセス権の設定(acl)

今回の目的、readとsearchを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/18f0c15e-74e5-4a4f-be5c-61170d613be4.png)

## バインドルール(userdn)

どのような権限をもってアクセスするかの設定、、、というとわかりにくいですが今回でいえば単に実行ユーザーの設定です。

グループ情報取得のために最初に作ったcn=group_reader,ou=AdnimUsers,dc=homeというユーザーを設定します。

検索ベースの横のリンクをクリックするとツリーで選択できるので便利です。後はエントリを検索の横の→ボタンを押すと利用可能なエントリにユーザー名がでてきますので、右に移してバインドルールの追加ボタンで確定させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/f7bdb296-21ba-4b2d-9166-123e9920d077.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/ef01344a-1ecd-4ba9-92ee-c16e5ccbaa80.png)

## 日時制限

必要があれば。なければそのままNext。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/23d2ef3f-a265-4fca-8294-00b941f5363b.png)

## 確認画面

ここで追加されるACIのテキストが表示されるので、確認・修正ができます。

ACLの追加ボタンを押して確定させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/ae31e149-641a-4ec4-a561-2f61ddce9c10.png)

最初GUIの使い方が分からず苦労しましたが、分かってしまえばざっと設定できて楽だし後から簡単に修正できるし便利ですね☺️

## ldapsearchで確認

上手く設定できていればladapsearchコマンドでグループの情報が取得できるはずです。

```bash
ldapsearch -x -H ldaps://localhost:636 -D "cn=group_reader,ou=AdminUsers,dc=home" -W -b "ou=Groups,dc=home" -s sub "(objectClass=groupOfNames)" 
Enter LDAP Password: 
# extended LDIF
#
# LDAPv3
# base <ou=Groups,dc=home> with scope subtree
# filter: (objectClass=groupOfNames)
# requesting: ALL
#

# SSHAllow, Groups, home
dn: cn=SSHAllow,ou=Groups,dc=home
objectClass: top
objectClass: groupOfNames
objectClass: posixGroup
cn: SSHAllow
gidNumber: 10000
memberUid: test_user

# SMBFull, Groups, home
dn: cn=SMBFull,ou=Groups,dc=home
objectClass: top
objectClass: groupOfNames
objectClass: posixGroup
cn: SMBFull
gidNumber: 10001
memberUid: test_user

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2
```

# Webアプリ(on Apache)で使用する

とりあえずCGIを動かして確認してみます。LDAPアクセス用の情報をSetEnvで設定しつつ、デフォルトのcgi-binの設定を書き換えてCGI実行とKerberos認証を埋め込みます。

```conf:/etc/httpd/conf/httpd.conf
...
SetEnv LDAP_DN "cn=group_reader,ou=AdminUsers,dc=home"
SetEnv LDAP_PASSWD "********"
...
<Directory "/var/www/cgi-bin">
    AllowOverride None
#    Options None
    Options +ExecCGI
    SetHandler cgi-script

    AuthType GSSAPI
    AuthName "Kerberos Login"
    GssapiAllowedMech krb5
    GssapiCredStore keytab:/etc/httpd/httpd.keytab
    Require valid-user

#    Require all granted
</Directory>
```

構文チェックしてOKならリロード。

```bash
$ apachectl configtest
$ systemctl reload httpd
```

チェック用CGIを置いてアクセスしてみます。

```python:/var/www/cgi-bin/check.py
#!/usr/bin/env python3
import os
from ldap3 import Connection, Server, ALL as LDAP_ALL, SIMPLE, Reader

# LDAPサーバーの情報
server_address = 'localhost'
server_port = 636
use_ssl = True
user_dn = os.getenv("LDAP_DN")
password = os.getenv("LDAP_PASSWD")
search_filter = '(objectClass=groupOfNames)'
base_dn = 'cn=SSHAllow,ou=Groups,dc=home'  # 拾いたいグループ
attributes = ['*']  # 拾いたいattribute

print("Content-Type: text/plain; charset=utf-8")
print()

server = Server(server_address, port=server_port, use_ssl=use_ssl, get_info=LDAP_ALL)

conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE)
if not conn.bind():
    print(f"バインドに失敗しました: {conn.last_error}")
    exit()

conn.search(base_dn, search_filter, search_scope='SUBTREE', attributes=attributes)
for entry in conn.entries:
    print(f"{entry}")

conn.unbind()
```

ブラウザでアクセスするとグループの情報が得られるので、`REMOTE_USER`とmemberUid等を使ってアクセスを許可するかどうか決定できます。

```:https://minipc.home/cgi-bin/check.py
DN: cn=SSHAllow,ou=Groups,dc=home - STATUS: Read - READ TIME: 
    cn: SSHAllow
    gidNumber: 10000
    memberUid: test_user
    objectClass: top
                 groupOfNames
                 posixGroup
```

なお仕事では、最後のunbind()を忘れてたり例外で通らないパスがあったりすると、管理部門のひとからお問い合わせ(という名の怒られ)がきます_(:3」∠)_