Argo CD における LDAP 認証と、LDAP グループを利用した権限制御

# はじめに

私の所属する [aslead DevOps](https://aslead.nri.co.jp/solution/aslead_devops.html) チームでは、Kubernetes のマニフェストファイルの適用の効率化、クラスタの状態を把握しやすくするツールとして、GitOps ツールである **[Argo CD](https://github.com/argoproj/argo-cd)** の検証を進めています。

開発環境や本番環境において、**クラスタを様々なチームで共用**したり、**インフラチームだけが特定の namespace を管理**する（例：istio に関する namespace は開発者には触らせたくない）等のケースを想定すると、認証認可の仕組みが必要になってきます。しかし、Argo CD で LDAP による認証認可を実現しようとしたところ、公式ドキュメントには詳細な設定方法は書かれておらず、やや苦戦しました。



本記事では、Argo CD に組み込まれている [Dex](https://github.com/dexidp/dex) を使った **LDAPによる認証、LDAPのグループによる認可** についてまとめていきます。

また、プロダクションのLDAPサーバに繋いで検証できないケースを想定し、**OpenLDAPでテスト用のLDAPサーバを立てる手順**を合わせて記載します。

## 本記事で使っている環境
* Docker CE 19.03.8
* kubectl v1.18.2
* Kind v0.8.1
  * Nodeのイメージ： kindest/node:v1.18.2 (Kubernetes v1.18.2 のイメージ) 
* Argo CD
  * サーバ: v1.6.1+159674e
  * CLI: v1.6.2+3d1f37b
* OpenLDAP 2.4.50 ([osixia/openldap:1.4.0](https://hub.docker.com/layers/osixia/openldap/1.4.0/images/sha256-d5b2f2b816b25a1b57033b34f5d48c91cc3161a7d041811a9032604030bad9db?context=explore))

## 本記事の前提
* k8s クラスタが構築済
* Argo CD がインストール済
* Argo CD サーバにブラウザからアクセスできること

# LDAP サーバの構築
動作確認用のLDAPサーバを作成します。

## ユーザとグループ
ユーザとグループの構成は以下の通り。

* ユーザ
  * alice (パスワード: alice)
  * bob (パスワード: bob)
* グループ
  * group1 (alice が所属）
  * group2 (bob が所属)  

![argocd-ldap-example.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696919/d129decf-2b18-9002-cfbf-4330ce5fdf31.png)

## OpenLDAP の Pod を作成する

以下の 3 つのマニフェストを作成します。

* 上記のユーザ/グループの構成をするための ConfigMap
* LDAPサーバを起動するための Deployment
* LDAPサーバに接続するための Service

```openldap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ldap-sample-data
data:
  sample-data.ldif: |
    version: 1
  
    dn: cn=group1,dc=example,dc=org
    objectClass: groupOfNames
    objectClass: top
    cn: group1
    member: uid=alice,ou=users,dc=example,dc=org

    dn: ou=users,dc=example,dc=org
    objectClass: organizationalUnit
    objectClass: top
    ou: users

    dn: uid=alice,ou=users,dc=example,dc=org
    objectClass: inetOrgPerson
    objectClass: organizationalPerson
    objectClass: person
    objectClass: top
    cn: alice
    sn: alice
    uid: alice
    userPassword:: YWxpY2U=

    dn: uid=bob,ou=users,dc=example,dc=org
    objectClass: inetOrgPerson
    objectClass: organizationalPerson
    objectClass: person
    objectClass: top
    cn: bob
    sn: bob
    uid: bob
    userPassword:: Ym9i

    dn: cn=group2,dc=example,dc=org
    objectClass: groupOfNames
    objectClass: top
    cn: group2
    member: uid=bob,ou=users,dc=example,dc=org
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openldap
spec:
  selector:
    matchLabels:
      app: openldap
  replicas: 1
  template:
    metadata:
      labels:
        app: openldap
    spec:
      containers:
        - name: openldap
          image: osixia/openldap:1.4.0
          args:
          - --loglevel
          - debug
          - --copy-service
          ports:
            - containerPort: 389
              name: ldap
          volumeMounts:
            - name: config-volume
              mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom/sample-data.ldif
              subPath: sample-data.ldif
      volumes:
        - name: config-volume
          configMap:
            name: ldap-sample-data
---
kind: Service
apiVersion: v1
metadata:
  name:  openldap-service
spec:
  selector:
    app: openldap
  type:  ClusterIP
  ports:
    - name:  ldap
      port:  389
      targetPort:  389
```

クラスタに適用します。

```bash
kubectl apply -f openldap.yaml
```

# Argo CD に Project と Application を作成する
Argo CD には、**Project** と **Application** という概念があります。
Application は、デプロイするアプリケーションを構成するマニフェストの集まりであり、**ソースとしてのGitへの参照と、デプロイ先クラスタ、ネームスペース** を定義します。Project は、**Application をグルーピング** する単位です。

k8s でのリソース名（kind）は Project が `AppProject`、Application は `Application` となります。

LDAP サーバに構成したグループに合わせ、以下の構成で Project と Application を作成します。

| Project | Application | 参照先 Git リポジトリ | 参照するパス |
|:--|:--|:--|:--|
| group1-project | group1-application | https://github.com/d-tsu/argocd-example.git | ldap-rbac-example/group1/ |
| group2-project | group2-application | https://github.com/d-tsu/argocd-example.git | ldap-rbac-example/group2/ |

マニフェストは以下の通りです。

```appproject.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: group1-project
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  description: group1 project
  sourceRepos:
  - '*'
  destinations:
  - namespace: group1-namespace
    server: https://kubernetes.default.svc
  clusterResourceWhitelist:
  - group: '*'
    kind: '*'
  orphanedResources:
    warn: false

---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: group2-project
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  description: group2 project
  sourceRepos:
  - '*'
  destinations:
  - namespace: group2-namespace
    server: https://kubernetes.default.svc
  clusterResourceWhitelist:
  - group: '*'
    kind: '*'
  orphanedResources:
    warn: false
```

```application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: group1-project
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: group1-project
  source:
    repoURL: 'https://github.com/d-tsu/argocd-example.git'
    path: ldap-rbac-example/group1
    targetRevision: HEAD
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: group1-namespace
  syncPolicy:
    automated: {}
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: group2-application
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: group2-application
  source:
    repoURL: 'https://github.com/d-tsu/argocd-example.git'
    path: ldap-rbac-example/group2
    targetRevision: HEAD
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: group2-namespace
  syncPolicy:
    automated: {}
```

クラスタに適用します。

```bash
kubectl -n argocd apply -f project.yaml
kubectl -n argocd apply -f application.yaml
```

# `argocd-cm` ConfigMap の作成
ここからが本題です。
LDAP サーバへの接続は `argocd` Namespace にある `argocd-cm` ConfigMap で設定します。

※`argocd-cm` は Argo CD に関する各種設定を行うためのリソースであり、シングルサインオンの設定以外にも、Argo CD への Git リポジトリの登録やローカルユーザの設定などが可能です。

Argo CD は外部に認証を委任するために [Dex](https://github.com/dexidp/dex) を組み込んでおり、 `argocd-cm` の `dex.config` で認証に関する設定ができます。[DexのLDAP接続に関するドキュメント](https://github.com/dexidp/dex/blob/master/Documentation/connectors/ldap.md) を参照し、`dex.config` 以下にLDAPに関する情報を設定していきます。

設定例は以下の通りです。各フィールドの詳細はコメントで記載しています。
ここでは、動作検証用途のため TLS 通信や証明書の検証処理は無効化していることに注意してください。

```argocd-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  # argocd サーバの URL（SSO設定する際は必須）
  url: https://argocd.example.com
  
  # dex の設定
  dex.config: | 
    connectors:
      - type: ldap
        id: ldap
        name: LDAP
        config: 
          # LDAPサーバのホスト名とポート番号
          # ポート番号が省略された場合：
          # insecureNoSSL: true の場合は ポート 389 が使われ、
          # startTLS: true の場合は TLS 通信するまで ポート 389 が使われる
          # それ以外の場合は ポート 636
          host: openldap-service.default.svc.cluster.local:389
          
          # TLS 通信を使わない場合は true
          insecureNoSSL: true

          # サーバ証明書の検証をスキップする場合は true
          insecureSkipVerify: true
          
          # startTLS を利用する場合は 
          # TLS 通信が可能か判断するまでは ldap:// プロトコル で通信し、
          # もし可能だったら ldaps:// プロトコルで通信をする
          # 参考：https://ja.wikipedia.org/wiki/STARTTLS
          startTLS: false

          # LDAPサーバで検索するための認証情報
          bindDN: cn=admin,dc=example,dc=org
          bindPW: admin

          # Dex のログイン画面のユーザ名に相当するテキストボックスに付くラベル
          # デフォルトは Username
          usernamePrompt: user

          # ユーザ検索に関する設定
          # この設定に基づき、ユーザとパスワードの照合が行われる
          userSearch:
            baseDN: ou=users,dc=example,dc=org      # ユーザ検索対象とするツリーのルート要素
            filter: "(objectClass=inetOrgPerson)"   # ユーザ検索時のフィルタ
            username: uid   # ログインに使われるユーザ名
            idAttr: uid     # どこに使われているか把握できていない。usernameと同一にしておくのが無難か
            emailAttr: uid  # この値がユーザ名（表示名）になる模様
            nameAttr: uid   # どこに使われているか把握できていない。usernameと同一にしておくのが無難か

          # グループ検索に関する設定
          # この設定に基づき、ユーザが所属しているグループが検索される
          # グループの groupAttr で指定した属性と、ユーザの userAttr で指定した属性がマッチするか確認される
          # ここでは、ユーザの DN と グループの member を突き合わせている
          groupSearch:
            baseDN: dc=example,dc=org            # グループ検索対象とするツリーのルート要素
            filter: "(objectClass=groupOfNames)" # グループ検索時のフィルタ
            userAttr: DN                         
            groupAttr: member
            nameAttr: cn                         # グループ名（RBAC設定で利用する）
```

クラスタに適用します。

```
kubectl -n argocd apply -f argocd-cm.yaml
```

## 参考：Dex 公式ドキュメントの誤りについて

[DexのLDAP接続に関するドキュメント](https://github.com/dexidp/dex/blob/master/Documentation/connectors/ldap.md) の `groupSearch` フィールドの例では、以下のように `userMatchers` が含まれた形式になっています。
しかし、**この設定ではグループ検索が動作しません**。 これは Dex のいくつかの Issue で言及されています。

* [LDAP groups from Active Directory #982](https://github.com/dexidp/dex/issues/982) 
* [user groups from Microsoft AD not returned using ldap dex connector #1655](https://github.com/dexidp/dex/issues/1655)

`userMatcher` 要素を取り除き、他の要素とフラットに記載することで正しく動作します。

```yaml
# これは動作しない
    groupSearch:
      baseDN: cn=groups,dc=freeipa,dc=example,dc=com
      filter: "(objectClass=group)"
      userMatchers:
      - userAttr: uid
        groupAttr: member
      nameAttr: name

# これは動作する
    groupSearch:
      baseDN: cn=groups,dc=freeipa,dc=example,dc=com
      filter: "(objectClass=group)"
      userAttr: uid
      groupAttr: member
      nameAttr: name
``` 

# `argocd-cm-rbac` ConfigMap の作成

RBAC の設定は `argocd` Namespace にある `argocd-cm-rbac` ConfigMap で設定します。
認可のポリシーの設定には、以下の2つの方法があります。

1. ユーザやグループに直接ポリシーを付与する
2. ロールにポリシーを付与し、ユーザやグループにロールを付与する

ポリシーの管理がしやすいように、基本的には 2. で設定することをおすすめします。

`argocd-cm-rbac` では、`policy.csv` 配下に以下の形式で認可のポリシーを定義できます。

**Application** の場合
`p, <role/user/group>, <resource>, <action>, <project>/<object>`

**それ以外** の場合
`p, <role/user/group>, <resource>, <action>, <object>`

**ロールを ロール/ユーザ/グループに紐付ける場合**
`g, <role/user/group>, <role>`

また、ポリシー行の末尾に カンマ続きで `allow` あるいは `deny` を定義できるようですが、Argo CD の公式ドキュメントやサンプルコードには説明がありませんでした。基本的には、暗黙的な deny をベースとして allow で許可することで、最小限の権限を与える方針が良いと思います。

暗黙的な deny は `policy.default` によって実現できます。
`policy.default` は、デフォルトでユーザに与える権限を指定できるフィールドですが、`policy.default` を定義しない、あるいは 空文字列 `""` を設定すると、ポリシーが何も与えられていない状態がデフォルトとなります。

```
# ロール名 role:group1-role に、 group1-project 配下の Application を参照する権限を許可する例
p, role:group1-role, applications, get, group1-project/*, allow

# ロール名 role:group1-role を、グループ group1 に付与する例
g, group1, role:group1-role
```

ポリシーを構成する各項目の意味と、設定できる値は以下のとおりです。

| 項目 | 意味 | 指定できる値 |
|:--|:--|:--|
| `<role/user/group>`  | ポリシーを割り当てる対象| ロール名 or ユーザ名 or グループ名  |
| `<resource>`  | ポリシーの設定対象とするリソース  | `clusters`, `projects`, `applications`, `repositories`, `certificates`  |
| `<action>`  | ポリシーで制御する操作  | `get`, `create`, `update`, `delete`, `sync`, `override`, `action`  |
| `<project>`  | プロジェクト  | プロジェクト名  |
| `<object>`  | リソースの名前  | リソース名（クラスタ名や、Application名など）  |

設定例は以下のとおりです。

```argocd-cm-rbac.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-rbac-cm
    app.kubernetes.io/part-of: argocd
data:
  policy.csv: |
    p, role:group1-role, applications, get, group1-project/*, allow
    p, role:group1-role, certificates, get, *, allow
    p, role:group1-role, clusters, get, *, allow
    p, role:group1-role, projects, get, *, allow
    p, role:group1-role, repositories, get, *, allow

    p, role:group2-role, applications, get, group2-project/*, allow
    p, role:group2-role, certificates, get, *, allow
    p, role:group2-role, clusters, get, *, allow
    p, role:group2-role, projects, get, *, allow
    p, role:group2-role, repositories, get, *, allow

    g, group1, role:group1-role
    g, group2, role:group2-role

  policy.default: ""
```

クラスタに適用します。

```
kubectl -n argocd apply -f argocd-cm-rbac.yaml
```



# 動作確認

まずは、LDAPサーバへの接続設定が正常に構成されているかを確認します。
正常に構成できている場合、ログイン画面に **LOGIN VIA LDAP** というボタンが表示されます。
表示されなかった場合は `argocd-cm` の設定に誤りがあるかもしれないので、設定内容を見直してください。

![Screenshot_2020-09-12 Argo CD - Help.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696919/0f885cdf-1862-448d-d170-f32ada1563e1.png)

**LOGIN VIA LDAP** をクリックすると、Dex が提供するログイン画面が表示されます。ここでは、ユーザ alice でログインしてみます。

![Screenshot_2020-09-12 dex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696919/5272ee91-7499-36f2-c55c-9b1b0f9bc053.png)

正常にログインできたら、以下のように group1-application が表示されるはずです。
Application が何も表示されなかった場合、あるいは group2-application も表示されている場合は `argocd-cm-rbac` の設定に誤りがあるかもしれないので、設定内容を見直してください。
![Screenshot_2020-09-12 Argo CD - Applications(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696919/1960c5cf-9deb-a833-8728-34c36d90f1f0.png)

User Info ページでユーザ情報を確認すると、alice が group1 に所属していることが確認できます。
![Screenshot_2020-09-16 Argo CD - User Info.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696919/88aea66e-9b7b-d375-8798-d30ce9612912.png)

同様に Bobで ログインすると以下の状態が確認できます。

* group2-application しか参照できない
* group2 に所属している

# まとめ

本記事では、**Argo CD における LDAP 認証の設定方法と、LDAP グループを利用した権限制御の設定方法**をまとめました。
認可のポリシーはシンプルな形式で設定できるので、一度ベースとなる `argocd-cm` を作ってしまえば、以降は設定を継ぎ足していくことで、所望の権限制御が実現できると思います。 
今回は、ConfigMap を直接 apply してしまいましたが、`argocd-cm`, `argocd-cm-rbac` なども GitOps で管理することで Argo CD 自体の設定を管理・監査できるようになります。Argo CD を宣言的に管理する方法は [公式ドキュメント](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/) に記載があるので、参照してみてください。